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

com.google.gwt.uibinder.rebind.XMLElement Maven / Gradle / Ivy

There is a newer version: 0.5.15
Show newest version
/*
 * Copyright 2008 Google Inc.
 * 
 * Licensed 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 com.google.gwt.uibinder.rebind;

import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.core.ext.typeinfo.TypeOracleException;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.resources.client.ImageResource;
import com.google.gwt.safehtml.shared.SafeHtml;
import com.google.gwt.uibinder.attributeparsers.AttributeParser;
import com.google.gwt.uibinder.attributeparsers.AttributeParsers;
import com.google.gwt.uibinder.elementparsers.SimpleInterpeter;

import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Text;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * A wrapper for {@link Element} that limits the way parsers can interact with
 * the XML document, and provides some convenience methods.
 * 

* The main function of this wrapper is to ensure that parsers can only read * elements and attributes by 'consuming' them, which removes the given value. * This allows for a natural hierarchy among parsers -- more specific parsers * will run first, and if they consume a value, less-specific parsers will not * see it. */ public class XMLElement { /** * Callback interface used by {@link #consumeInnerHtml(Interpreter)} and * {@link #consumeChildElements(Interpreter)}. */ public interface Interpreter { /** * Given an XMLElement, return its filtered value. * * @throws UnableToCompleteException on error */ T interpretElement(XMLElement elem) throws UnableToCompleteException; } /** * Extends {@link Interpreter} with a method to be called after all elements * have been processed. */ public interface PostProcessingInterpreter extends Interpreter { String postProcess(String consumedText) throws UnableToCompleteException; } /** * Represents the source location where the XMLElement was declared. */ public static class Location { private final String systemId; private final int lineNumber; public Location(String systemId, int lineNumber) { this.systemId = systemId; this.lineNumber = lineNumber; } public int getLineNumber() { return lineNumber; } public String getSystemId() { return systemId; } /** * For debugging use only. */ @Override public String toString() { return systemId + ":" + lineNumber; } } static final String LOCATION_KEY = "gwtLocation"; private static final Set NO_END_TAG = new HashSet(); private static final String[] EMPTY = new String[]{}; private static void clearChildren(Element elem) { // TODO(rjrjr) I'm nearly positive that anywhere this is called // we should instead be calling assertNoBody Node child; while ((child = elem.getFirstChild()) != null) { elem.removeChild(child); } } private final Element elem; private final AttributeParsers attributeParsers; private final TypeOracle oracle; private final MortalLogger logger; private final String debugString; private final DesignTimeUtils designTime; private final XMLElementProvider provider; private JType booleanType; private JType imageResourceType; private JType doubleType; private JType intType; private JType safeHtmlType; private JType stringType; { // from com/google/gxp/compiler/schema/html.xml NO_END_TAG.add("area"); NO_END_TAG.add("base"); NO_END_TAG.add("basefont"); NO_END_TAG.add("br"); NO_END_TAG.add("col"); NO_END_TAG.add("frame"); NO_END_TAG.add("hr"); NO_END_TAG.add("img"); NO_END_TAG.add("input"); NO_END_TAG.add("isindex"); NO_END_TAG.add("link"); NO_END_TAG.add("meta"); NO_END_TAG.add("param"); NO_END_TAG.add("wbr"); } XMLElement(Element elem, AttributeParsers attributeParsers, TypeOracle oracle, MortalLogger logger, DesignTimeUtils designTime, XMLElementProvider provider) { this.elem = elem; this.attributeParsers = attributeParsers; this.oracle = oracle; this.logger = logger; this.designTime = designTime; this.provider = provider; this.debugString = getOpeningTag(); } /** * Ensure that the receiver has no attributes left. * * @throws UnableToCompleteException if it does */ public void assertNoAttributes() throws UnableToCompleteException { int numAtts = getAttributeCount(); if (numAtts == 0) { return; } StringBuilder b = new StringBuilder(); for (int i = 0; i < numAtts; i++) { if (i > 0) { b.append(", "); } b.append('"').append(getAttribute(i).getName()).append('"'); } logger.die(this, "Unexpected attributes: %s", b); } /** * Require that the receiver's body is empty of text and has no child nodes. * * @throws UnableToCompleteException if it isn't */ public void assertNoBody() throws UnableToCompleteException { consumeChildElements(new Interpreter() { public Boolean interpretElement(XMLElement elem) throws UnableToCompleteException { logger.die(elem, "Found unexpected child element"); return false; // unreachable } }); assertNoText(); } /** * Require that the receiver's body is empty of text. * * @throws UnableToCompleteException if it isn't */ public void assertNoText() throws UnableToCompleteException { SimpleInterpeter nullInterpreter = new SimpleInterpeter(null); String s = consumeInnerTextEscapedAsHtmlStringLiteral(nullInterpreter); if (!"".equals(s)) { logger.die(this, "Unexpected text in element: \"%s\"", s); } } /** * Consumes the given attribute as a literal or field reference. The type * parameter is required to determine how the value is parsed and validated. * * @param name the attribute's full name (including prefix) * @param type the type this attribute is expected to provide * @return the attribute's value as a Java expression, or null if it is not * set * @throws UnableToCompleteException on parse failure */ public String consumeAttribute(String name, JType type) throws UnableToCompleteException { return consumeAttributeWithDefault(name, null, type); } /** * Consumes the given attribute as a literal or field reference. The type * parameter is required to determine how the value is parsed and validated. * * @param name the attribute's full name (including prefix) * @param defaultValue the value to @return if the attribute was unset * @param type the type this attribute is expected to provide * @return the attribute's value as a Java expression, or the given default if * it was unset * @throws UnableToCompleteException on parse failure */ public String consumeAttributeWithDefault(String name, String defaultValue, JType type) throws UnableToCompleteException { return consumeAttributeWithDefault(name, defaultValue, new JType[]{type}); } /** * Like {@link #consumeAttributeWithDefault(String, String, JType)}, but * accommodates more complex type signatures. */ public String consumeAttributeWithDefault(String name, String defaultValue, JType... types) throws UnableToCompleteException { if (!hasAttribute(name)) { if (defaultValue != null) { designTime.putAttribute(this, name + ".default", defaultValue); } return defaultValue; } AttributeParser parser = attributeParsers.getParser(types); return consumeAttributeWithParser(name, parser); } /** * Convenience method for parsing the named attribute as a boolean value or * reference. * * @return an expression that will evaluate to a boolean value in the * generated code, or null if there is no such attribute * * @throws UnableToCompleteException on unparseable value */ public String consumeBooleanAttribute(String name) throws UnableToCompleteException { return consumeAttribute(name, getBooleanType()); } /** * Convenience method for parsing the named attribute as a boolean value or * reference. * * @param defaultValue value to return if attribute was not set * @return an expression that will evaluate to a boolean value in the * generated code, or defaultValue if there is no such attribute * * @throws UnableToCompleteException on unparseable value */ public String consumeBooleanAttribute(String name, boolean defaultValue) throws UnableToCompleteException { return consumeAttributeWithDefault(name, Boolean.toString(defaultValue), getBooleanType()); } /** * Consumes the named attribute as a boolean expression. This will not accept * {field.reference} expressions. Useful for values that must be resolved at * compile time, such as generated annotation values. * * @return {@link Boolean#TRUE}, {@link Boolean#FALSE}, or null if no such * attribute * * @throws UnableToCompleteException on unparseable value */ public Boolean consumeBooleanConstantAttribute(String name) throws UnableToCompleteException { String value = consumeRawAttribute(name); if (value == null) { return null; } if (value.equals("true") || value.equals("false")) { return Boolean.valueOf(value); } logger.die(this, "%s must be \"true\" or \"false\"", name); return null; // unreachable } /** * Consumes and returns all child elements. * * @throws UnableToCompleteException if extra text nodes are found */ public Iterable consumeChildElements() throws UnableToCompleteException { Iterable rtn = consumeChildElementsNoEmptyCheck(); assertNoText(); return rtn; } /** * Consumes and returns all child elements selected by the interpreter. Note * that text nodes are not elements, and so are not presented for * interpretation, and are not consumed. * * @param interpreter Should return true for any child that should be consumed * and returned by the consumeChildElements call * @throws UnableToCompleteException */ public Collection consumeChildElements(Interpreter interpreter) throws UnableToCompleteException { List elements = new ArrayList(); List doomed = new ArrayList(); NodeList childNodes = elem.getChildNodes(); for (int i = 0; i < childNodes.getLength(); ++i) { Node childNode = childNodes.item(i); if (childNode.getNodeType() == Node.ELEMENT_NODE) { XMLElement childElement = provider.get((Element) childNode); if (interpreter.interpretElement(childElement)) { elements.add(childElement); doomed.add(childNode); } } } for (Node n : doomed) { elem.removeChild(n); } return elements; } /** * Convenience method for parsing the named attribute as an ImageResource * value or reference. * * @return an expression that will evaluate to an ImageResource value in the * generated code, or null if there is no such attribute * @throws UnableToCompleteException on unparseable value */ public String consumeImageResourceAttribute(String name) throws UnableToCompleteException { return consumeAttribute(name, getImageResourceType()); } /** * Consumes all child elements, and returns an HTML interpretation of them. * Trailing and leading whitespace is trimmed. *

* Each element encountered will be passed to the given Interpreter for * possible replacement. Escaping is performed to allow the returned text to * serve as a Java string literal used as input to a setInnerHTML call. *

* This call requires an interpreter to make sense of any special children. * The odds are you want to use * {@link com.google.gwt.uibinder.elementparsers.HtmlInterpreter} for an HTML * value, or {@link com.google.gwt.uibinder.elementparsers.TextInterpreter} * for text. * * @param interpreter Called for each element, expected to return a string * replacement for it, or null if it should be left as is */ public String consumeInnerHtml(Interpreter interpreter) throws UnableToCompleteException { if (interpreter == null) { throw new NullPointerException("interpreter must not be null"); } StringBuffer buf = new StringBuffer(); GetInnerHtmlVisitor.getEscapedInnerHtml(elem, buf, interpreter, provider); clearChildren(elem); return buf.toString().trim(); } /** * Refines {@link #consumeInnerHtml(Interpreter)} to handle * PostProcessingInterpreter. */ public String consumeInnerHtml(PostProcessingInterpreter interpreter) throws UnableToCompleteException { String html = consumeInnerHtml((Interpreter) interpreter); return interpreter.postProcess(html); } /** * Refines {@link #consumeInnerTextEscapedAsHtmlStringLiteral(Interpreter)} to * handle PostProcessingInterpreter. */ public String consumeInnerText(PostProcessingInterpreter interpreter) throws UnableToCompleteException { String text = consumeInnerTextEscapedAsHtmlStringLiteral(interpreter); return interpreter.postProcess(text); } /** * Consumes all child text nodes, and asserts that this element held only * text. Trailing and leading whitespace is trimmed, and escaped for use as a * string literal. Notice that HTML entities in the text are also escaped *

* This call requires an interpreter to make sense of any special children. * The odds are you want to use * {@link com.google.gwt.uibinder.elementparsers.TextInterpreter} * * @throws UnableToCompleteException If any elements present are not consumed * by the interpreter */ public String consumeInnerTextEscapedAsHtmlStringLiteral(Interpreter interpreter) throws UnableToCompleteException { return consumeInnerTextEscapedAsHtmlStringLiteral(interpreter, true); } /** * Consumes all child text nodes, and asserts that this element held only * text. Trailing and leading whitespace is trimmed, and escaped for use as a * string literal. Notice that HTML entities in the text are NOT escaped *

* This call requires an interpreter to make sense of any special children. * The odds are you want to use * {@link com.google.gwt.uibinder.elementparsers.TextInterpreter} * * @throws UnableToCompleteException If any elements present are not consumed * by the interpreter */ public String consumeInnerTextEscapedAsStringLiteral(Interpreter interpreter) throws UnableToCompleteException { return consumeInnerTextEscapedAsHtmlStringLiteral(interpreter, false); } /** * Convenience method for parsing the named attribute as a CSS length value. * * @return a (double, Unit) pair literal, an expression that will evaluate to * such a pair in the generated code, or null if there is no such * attribute * * @throws UnableToCompleteException on unparseable value */ public String consumeLengthAttribute(String name) throws UnableToCompleteException { return consumeAttributeWithDefault(name, null, new JType[]{getDoubleType(), getUnitType()}); } /** * Consumes all attributes, and returns a string representing the entire * opening tag. E.g., "

" */ public String consumeOpeningTag() { String rtn = getOpeningTag(); for (int i = getAttributeCount() - 1; i >= 0; i--) { getAttribute(i).consumeRawValue(); } return rtn; } /** * Consumes the named attribute and parses it to an unparsed, unescaped array * of Strings. The strings in the attribute may be comma or space separated * (or a mix of both). * * @return array of String, empty if the attribute was not set. */ public String[] consumeRawArrayAttribute(String name) { String raw = consumeRawAttribute(name, null); if (raw == null) { return EMPTY; } return raw.split("[,\\s]+"); } /** * Consumes the given attribute and returns its trimmed value, or null if it * was unset. The returned string is not escaped. * * @param name the attribute's full name (including prefix) * @return the attribute's value, or "" */ public String consumeRawAttribute(String name) { if (!elem.hasAttribute(name)) { return null; } String value = elem.getAttribute(name); elem.removeAttribute(name); return value.trim(); } /** * Consumes the given attribute and returns its trimmed value, or the given * default value if it was unset. The returned string is not escaped. * * @param name the attribute's full name (including prefix) * @param defaultValue the value to return if the attribute was unset * @return the attribute's value, or defaultValue */ public String consumeRawAttribute(String name, String defaultValue) { String value = consumeRawAttribute(name); if (value == null) { return defaultValue; } return value; } /** * Consumes the given required attribute as a literal or field reference. The * types parameters are required to determine how the value is parsed and * validated. * * @param name the attribute's full name (including prefix) * @param types the type(s) this attribute is expected to provide * @return the attribute's value as a Java expression * @throws UnableToCompleteException on parse failure, or if the attribute is * empty or unspecified */ public String consumeRequiredAttribute(String name, JType... types) throws UnableToCompleteException { if (!hasAttribute(name)) { failRequired(name); } AttributeParser parser = attributeParsers.getParser(types); String value = parser.parse(this, consumeRequiredRawAttribute(name)); designTime.putAttribute(this, name, value); return value; } /** * Convenience method for parsing the named required attribute as a double * value or reference. * * @return a double literal, an expression that will evaluate to a double * value in the generated code * * @throws UnableToCompleteException on unparseable value, or if the attribute * is empty or unspecified */ public String consumeRequiredDoubleAttribute(String name) throws UnableToCompleteException { return consumeRequiredAttribute(name, getDoubleType()); } /** * Convenience method for parsing the named required attribute as a integer * value or reference. * * @return a integer literal, an expression that will evaluate to a integer * value in the generated code * * @throws UnableToCompleteException on unparseable value, or if the attribute * is empty or unspecified */ public String consumeRequiredIntAttribute(String name) throws UnableToCompleteException { return consumeRequiredAttribute(name, getIntType()); } /** * Consumes the named attribute, or dies if it is missing. */ public String consumeRequiredRawAttribute(String name) throws UnableToCompleteException { String value = consumeRawAttribute(name); if (value == null) { failRequired(name); } return value; } /** * Convenience method for parsing the named attribute as a * {@link com.google.gwt.safehtml.shared.SafeHtml SafeHtml} value or * reference. * * @return an expression that will evaluate to a * {@link com.google.gwt.safehtml.shared.SafeHtml SafeHtml} value in * the generated code, or null if there is no such attribute * @throws UnableToCompleteException on unparseable value */ public String consumeSafeHtmlAttribute(String name) throws UnableToCompleteException { return consumeAttribute(name, getSafeHtmlType()); } /** * Consumes an attribute as either a SafeUri or a String. Used in HTML * contexts. * * @return an expression that will evaluate to a SafeUri value in the * generated code, or null if there is no such attribute * @throws UnableToCompleteException on unparseable value */ public String consumeSafeUriOrStringAttribute(String name) throws UnableToCompleteException { return consumeAttributeWithParser(name, attributeParsers.getSafeUriInHtmlParser()); } /** * Consumes a single child element, ignoring any text nodes and throwing an * exception if no child is found, or more than one child element is found. * * @throws UnableToCompleteException on no children, or too many */ public XMLElement consumeSingleChildElement() throws UnableToCompleteException { XMLElement ret = null; for (XMLElement child : consumeChildElements()) { if (ret != null) { logger.die(this, "Element may only contain a single child element, but " + "found %s and %s.", ret, child); } ret = child; } if (ret == null) { logger.die(this, "Element must have a single child element"); } return ret; } /** * Consumes the named attribute and parses it to an array of String * expressions. The strings in the attribute may be comma or space separated * (or a mix of both). * * @return array of String expressions, empty if the attribute was not set. * @throws UnableToCompleteException on unparseable value */ public String[] consumeStringArrayAttribute(String name) throws UnableToCompleteException { AttributeParser parser = attributeParsers.getParser(getStringType()); String[] strings = consumeRawArrayAttribute(name); for (int i = 0; i < strings.length; i++) { strings[i] = parser.parse(this, strings[i]); } designTime.putAttribute(this, name, strings); return strings; } /** * Convenience method for parsing the named attribute as a String value or * reference. * * @return an expression that will evaluate to a String value in the generated * code, or null if there is no such attribute * @throws UnableToCompleteException on unparseable value */ public String consumeStringAttribute(String name) throws UnableToCompleteException { return consumeAttribute(name, getStringType()); } /** * Convenience method for parsing the named attribute as a String value or * reference. * * @return an expression that will evaluate to a String value in the generated * code, or the given defaultValue if there is no such attribute * @throws UnableToCompleteException on unparseable value */ public String consumeStringAttribute(String name, String defaultValue) throws UnableToCompleteException { return consumeAttributeWithDefault(name, defaultValue, getStringType()); } /** * Returns the unprocessed, unescaped, raw inner text of the receiver. Dies if * the receiver has non-text children. *

* You probably want to use * {@link #consumeInnerTextEscapedAsHtmlStringLiteral} instead. * * @return the text * @throws UnableToCompleteException if it held anything other than text nodes */ public String consumeUnescapedInnerText() throws UnableToCompleteException { final NodeList children = elem.getChildNodes(); if (children.getLength() < 1) { return ""; } if (children.getLength() > 1 || Node.TEXT_NODE != children.item(0).getNodeType()) { logger.die(this, "Element must contain only text"); } Text t = (Text) children.item(0); return t.getTextContent(); } /** * Get the attribute at the given index. If you are consuming attributes, * remember to traverse them in reverse. */ public XMLAttribute getAttribute(int i) { return new XMLAttribute(XMLElement.this, (Attr) elem.getAttributes().item(i)); } /** * Get the attribute with the given name. * * @return the attribute, or null if there is none of that name */ public XMLAttribute getAttribute(String name) { Attr attr = elem.getAttributeNode(name); if (attr == null) { return null; } return new XMLAttribute(this, attr); } /** * Returns the number of attributes this element has. */ public int getAttributeCount() { return elem.getAttributes().getLength(); } public String getClosingTag() { if (NO_END_TAG.contains(elem.getTagName())) { return ""; } return String.format("", elem.getTagName()); } /** * Returns the design time path of this element, in form of indexes from root, * such as "0/0/1/0". */ public String getDesignTimePath() { return designTime.getPath(elem); } /** * Gets this element's local name (sans namespace prefix). */ public String getLocalName() { return elem.getLocalName(); } public Location getLocation() { return (Location) elem.getUserData(LOCATION_KEY); } /** * Gets this element's namespace URI. */ public String getNamespaceUri() { return elem.getNamespaceURI(); } /** * Returns the parent element, or null if parent is null or a node type other * than Element. */ public XMLElement getParent() { Node parent = elem.getParentNode(); if (parent == null || Node.ELEMENT_NODE != parent.getNodeType()) { return null; } return provider.get((Element) parent); } public String getPrefix() { return elem.getPrefix(); } /** * Determines whether the element has a given attribute. */ public boolean hasAttribute(String name) { return elem.hasAttribute(name); } public boolean hasChildNodes() { return elem.hasChildNodes(); } public String lookupPrefix(String prefix) { return elem.lookupPrefix(prefix); } public void setAttribute(String name, String value) { elem.setAttribute(name, value); } @Override public String toString() { return debugString; } private String consumeAttributeWithParser(String name, AttributeParser parser) throws UnableToCompleteException { String value = parser.parse(this, consumeRawAttribute(name)); designTime.putAttribute(this, name, value); return value; } private Iterable consumeChildElementsNoEmptyCheck() { try { Iterable rtn = consumeChildElements(new SimpleInterpeter(true)); return rtn; } catch (UnableToCompleteException e) { throw new RuntimeException("Impossible exception", e); } } /** * Consumes all child text nodes, and asserts that this element held only * text. Trailing and leading whitespace is trimmed, and escaped for use as a * string literal. If escapeHtmlEntities is true, HTML Entities are also escaped. *

* This call requires an interpreter to make sense of any special children. * The odds are you want to use * {@link com.google.gwt.uibinder.elementparsers.TextInterpreter} * * @throws UnableToCompleteException If any elements present are not consumed * by the interpreter */ private String consumeInnerTextEscapedAsHtmlStringLiteral(Interpreter interpreter, boolean escapeHtmlEntities) throws UnableToCompleteException { if (interpreter == null) { throw new NullPointerException("interpreter must not be null"); } StringBuffer buf = new StringBuffer(); if (escapeHtmlEntities) { GetInnerTextVisitor.getHtmlEscapedInnerText(elem, buf, interpreter, provider); } else { GetInnerTextVisitor.getEscapedInnerText(elem, buf, interpreter, provider); } // Make sure there are no children left but empty husks for (XMLElement child : consumeChildElementsNoEmptyCheck()) { if (child.hasChildNodes() || child.getAttributeCount() > 0) { logger.die(this, "Illegal child %s in a text-only context. " + "Perhaps you are trying to use unescaped HTML " + "where text is required, as in a HasText widget?", child); } } clearChildren(elem); return buf.toString().trim(); } private void failRequired(String name) throws UnableToCompleteException { logger.die(this, "Missing required attribute \"%s\"", name); } private JType getBooleanType() { if (booleanType == null) { try { booleanType = oracle.parse("boolean"); } catch (TypeOracleException e) { throw new RuntimeException(e); } } return booleanType; } private JType getDoubleType() { if (doubleType == null) { try { doubleType = oracle.parse("double"); } catch (TypeOracleException e) { throw new RuntimeException(e); } } return doubleType; } private JType getImageResourceType() { if (imageResourceType == null) { imageResourceType = oracle.findType(ImageResource.class.getCanonicalName()); } return imageResourceType; } private JType getIntType() { if (intType == null) { try { intType = oracle.parse("int"); } catch (TypeOracleException e) { throw new RuntimeException(e); } } return intType; } private String getOpeningTag() { StringBuilder b = new StringBuilder().append("<").append(elem.getTagName()); NamedNodeMap attrs = elem.getAttributes(); for (int i = 0; i < attrs.getLength(); i++) { Attr attr = (Attr) attrs.item(i); b.append(String.format(" %s='%s'", attr.getName(), UiBinderWriter.escapeAttributeText(attr.getValue()))); } b.append(">"); return b.toString(); } private JType getSafeHtmlType() { if (safeHtmlType == null) { safeHtmlType = oracle.findType(SafeHtml.class.getName()); } return safeHtmlType; } private JType getStringType() { if (stringType == null) { stringType = oracle.findType(String.class.getCanonicalName()); } return stringType; } private JClassType getUnitType() { return oracle.findType(Unit.class.getCanonicalName()).isEnum(); } // laaglu public Element getElement() { return elem; } // laaglu }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy