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

org.daisy.pipeline.braille.css.impl.BrailleCssSerializer Maven / Gradle / Ivy

The newest version!
package org.daisy.pipeline.braille.css.impl;

import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;

import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;

import cz.vutbr.web.css.CSSProperty;
import cz.vutbr.web.css.CSSProperty.CounterIncrement;
import cz.vutbr.web.css.CSSProperty.CounterSet;
import cz.vutbr.web.css.CSSProperty.CounterReset;
import cz.vutbr.web.css.Declaration;
import cz.vutbr.web.css.NodeData;
import cz.vutbr.web.css.Rule;
import cz.vutbr.web.css.RuleBlock;
import cz.vutbr.web.css.RulePage;
import cz.vutbr.web.css.Selector;
import cz.vutbr.web.css.Selector.Combinator;
import cz.vutbr.web.css.Term;
import cz.vutbr.web.css.TermFunction;
import cz.vutbr.web.css.TermString;
import cz.vutbr.web.css.TermURI;
import cz.vutbr.web.csskit.OutputUtil;

import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;

import org.daisy.braille.css.BrailleCSSParserFactory.Context;
import org.daisy.braille.css.BrailleCSSProperty.Content;
import org.daisy.braille.css.BrailleCSSProperty.StringSet;
import org.daisy.common.file.URLs;
import org.daisy.pipeline.braille.css.impl.BrailleCssParser.ParsedDeclaration;
import org.daisy.pipeline.braille.css.impl.BrailleCssParser.ParsedDeclarations;
import org.daisy.pipeline.braille.css.impl.ContentList.AttrFunction;
import org.daisy.pipeline.braille.css.impl.ContentList.ContentFunction;
import org.daisy.pipeline.braille.css.impl.ContentList.CounterFunction;
import org.daisy.pipeline.braille.css.impl.ContentList.CounterStyle;
import org.daisy.pipeline.braille.css.impl.ContentList.FlowFunction;
import org.daisy.pipeline.braille.css.impl.ContentList.LeaderFunction;
import org.daisy.pipeline.braille.css.impl.ContentList.StringFunction;
import org.daisy.pipeline.braille.css.impl.ContentList.TextFunction;
import org.daisy.pipeline.braille.css.impl.ContentList.URL;
import org.daisy.braille.css.InlineStyle;
import org.daisy.braille.css.InlineStyle.RuleMainBlock;
import org.daisy.braille.css.InlineStyle.RuleRelativeBlock;
import org.daisy.braille.css.LanguageRange;
import org.daisy.braille.css.PropertyValue;
import org.daisy.pipeline.css.CssSerializer;

public final class BrailleCssSerializer {

	private BrailleCssSerializer() {}

	/* =================================================== */
	/* toString                                            */
	/* =================================================== */

	public static String toString(Term term) {
		if (term instanceof TextTransformList ||
		    term instanceof ContentList ||
		    term instanceof StringSetList ||
		    term instanceof CounterSetList)
			return serializeTermList((List>)term);
		else if (term instanceof AttrFunction) {
			AttrFunction f = (AttrFunction)term;
			String s = "attr(" + toString(f.name);
			if (f.asURL)
				s = s + " url";
			s += ")";
			return s; }
		else if (term instanceof ContentFunction) {
			ContentFunction f = (ContentFunction)term;
			if (f.target.isPresent())
				return "target-content(" + toString(f.target.get()) + ")";
			else
				return "content()"; }
		else if (term instanceof StringFunction) {
			StringFunction f = (StringFunction)term;
			if (f.target.isPresent())
				return "target-string(" + toString(f.target.get()) + ", " + toString(f.name) + ")";
			else {
				String s = "string(" + toString(f.name);
				if (f.scope.isPresent())
					s = s + ", " + f.scope.get().toString();
				s += ")";
				return s; }}
		else if (term instanceof CounterFunction) {
			CounterFunction f = (CounterFunction)term;
			String s = "";
			if (f.target.isPresent())
				s = "target-counter(" + toString(f.target.get()) + ", " + toString(f.name);
			else
				s = "counter(" + toString(f.name);
			if (f.style.isPresent())
				s = s + ", " + toString(f.style.get());
			s += ")";
			return s; }
		else if (term instanceof TextFunction) {
			TextFunction f = (TextFunction)term;
			return "target-text(" + toString(f.target) + ")"; }
		else if (term instanceof LeaderFunction) {
			LeaderFunction f = (LeaderFunction)term;
			String s = "leader(" + toString(f.pattern);
			if (f.position.isPresent())
				s = s + ", " + toString(f.position.get());
			if (f.alignment.isPresent())
				s = s + ", " + f.alignment.get().toString();
			s += ")";
			return s; }
		else if (term instanceof FlowFunction) {
			FlowFunction f = (FlowFunction)term;
			String s = "flow(" + toString(f.from);
			if (f.scope.isPresent())
				s = s + ", " + f.scope.get().toString();
			s += ")";
			return s; }
		else
			return CssSerializer.toString(term, t -> toString(t));
	}

	public static String toString(Declaration declaration) {
		if (declaration instanceof PropertyValue)
			return declaration.getProperty() + ": " + serializePropertyValue((PropertyValue)declaration);
		else
			return declaration.getProperty() + ": " + serializeTermList(declaration);
	}

	public static String toString(BrailleCssStyle style) {
		if (style.serialized == null) {
			style.serialized = toString(style, null, null);
			// cache
			if (style.parser != null)
				style.parser.cache.put(style.context, style.serialized, style);
		} else {
			// access cache to keep entry longer in it
			if (style.parser != null)
				style.parser.cache.get(style.context, style.serialized);
		}
		return style.serialized;
	}

	/**
	 * @param relativeTo If not {@code null}, include only those declarations that are needed
	 *                   to reconstruct {@code style} with {@code relativeTo} as the parent
	 *                   style. Relativizes even if the parent style is empty.
	 */
	public static String toString(BrailleCssStyle style, BrailleCssStyle relativeTo) {
		if (relativeTo == null)
			return toString(style);
		String s = toString(style.relativize(relativeTo));
		// cache
		if (style.parser != null)
			style.parser.cache.put(style.context,
			                       s,
			                       relativeTo.declarations != null
			                           ? (ParsedDeclarations)relativeTo.declarations
			                           : ParsedDeclarations.EMPTY,
			                       true,
			                       style);
		return s;
	}

	public static String toString(BrailleCssStyle style, String indentation) {
		// this function does update the cache, nor does it store the string in the style object, as
		// it's meant to pretty print a style (for the purpose of showing in temporary files or log
		//messages)
		return toString(style, null, indentation);
	}

	private static String toString(BrailleCssStyle style, String base, String indent) {
		StringBuilder b = new StringBuilder();
		StringBuilder rel = new StringBuilder();
		if ("".equals(indent)) indent = null;
		String newline = indent != null ? "\n" : " ";
		if (style.declarations != null) {
			b.append(serializeDeclarationList(style.declarations, ";" + newline));
			if (indent != null && b.length() > 0) b.append(";");
		}
		if (style.nestedStyles != null)
			for (Map.Entry e : style.nestedStyles.entrySet()) {
				if (base != null && e.getKey().startsWith("&")) {
					if (rel.length() > 0) rel.append(newline);
					rel.append(toString(e.getValue(), base + e.getKey().substring(1), indent));
				} else {
					if (b.length() > 0) {
						if (indent == null && b.charAt(b.length() - 1) != '}') b.append(";");
						b.append(newline);
					}
					b.append(toString(e.getValue(), e.getKey(), indent));
				}
			}
		if (base != null && b.length() > 0) {
			if (indent != null) {
				String s = b.toString();
				s = indent + s.trim().replaceAll("\n", "\n" + indent);
				b.setLength(0);
				b.append(s);
			}
			b.insert(0, base + " {" + newline);
			b.append(newline + "}");
		}
		if (rel.length() > 0) {
			if (b.length() > 0) {
				if (indent == null && b.charAt(b.length() - 1) != '}') b.append(";");
				b.append(newline);
			}
			b.append(rel);
		}
		return b.toString();
	}

	public static String toString(NodeData style) {
		List declarations = new ArrayList<>();
		for (String p : style.getPropertyNames()) {
			String v = serializePropertyValue(style, p);
			if (v != null)
				declarations.add(p + ": " + v);
		}
		Collections.sort(declarations);
		StringBuilder s = new StringBuilder();
		Iterator it = declarations.iterator();
		while (it.hasNext()) {
			s.append(it.next());
			if (it.hasNext()) s.append("; ");
		}
		return s.toString();
	}

	public static String serializePropertyValue(NodeData style, String property) {
		return serializePropertyValue(style, property, true);
	}

	public static String serializePropertyValue(NodeData style, String property, boolean includeInherited) {
		Term value = style.getValue(property, includeInherited);
		if (value != null)
			return toString(value);
		else {
			CSSProperty p = style.getProperty(property, includeInherited);
			return p != null ? p.toString() : null;
		}
	}

	public static String serializePropertyValue(PropertyValue propValue) {
		if (propValue instanceof ParsedDeclaration)
			return ((ParsedDeclaration)propValue).valueToString(); // this may trigger caching
		else {
			Term value = propValue.getValue();
			if (value != null)
				return toString(value);
			else
				return propValue.getCSSProperty().toString();
		}
	}

	public static String toString(RuleMainBlock rule) {
		return serializeDeclarationList(rule);
	}

	public static String toString(RuleRelativeBlock rule) {
		StringBuilder b = new StringBuilder();
		boolean first = true;
		for (Selector s : rule.getSelector()) {
			Combinator c = s.getCombinator();
			if (first) {
				if (c == null)
					b.append("&");
				else if (c != Combinator.CHILD)
					b.append(c.value());
				first = false;
			} else if (c != null) // should always be true
				b.append(c.value());
			b = OutputUtil.appendList(b, s, OutputUtil.EMPTY_DELIM);
		}
		b.append(" { ");
		b.append(toString((RuleBlock>)rule));
		b.append(" }");
		return b.toString();
	}

	public static String toString(RuleBlock> ruleBlock) {
		StringBuilder b = new StringBuilder();
		b.append(serializeDeclarationList(Iterables.filter(ruleBlock, Declaration.class)));
		for (Rule r : ruleBlock)
			if (r instanceof Declaration);
			else if (r instanceof RulePage)
				b.append(toString((RulePage)r));
			else
				throw new RuntimeException("not implemented");
		return b.toString();
	}

	public static String toString(RulePage page, BrailleCssParser parser) {
		return toString(BrailleCssStyle.of(parser, page));
	}

	public static String serializeRuleBlockList(Iterable>> ruleBlocks) {
		String b = null;
		for (RuleBlock> r : ruleBlocks) {
			String s;
			if (r instanceof RuleMainBlock)
				s = BrailleCssSerializer.toString((RuleMainBlock)r);
			else if (r instanceof RuleRelativeBlock)
				s = BrailleCssSerializer.toString((RuleRelativeBlock)r);
			else
				s = BrailleCssSerializer.toString(r);
			if (!s.isEmpty())
				if (b == null)
					b = s;
				else {
					if (!(b.endsWith("}") || b.endsWith(";")))
						b = b + ";";
					b += " ";
					b += s; }}
		if (b == null) b = "";
		return b;
	}

	public static String serializeTermList(Collection> termList) {
		return CssSerializer.serializeTermList(termList, t -> toString(t));
	}

	public static String serializeLanguageRanges(List languageRanges) {
		return OutputUtil.appendList(new StringBuilder(), languageRanges, OutputUtil.SELECTOR_DELIM).toString();
	}

	public static String serializeDeclarationList(Iterable declarations) {
		return serializeDeclarationList(declarations, "; ");
	}

	private static String serializeDeclarationList(Iterable declarations, String separator) {
		List sortedDeclarations = new ArrayList<>();
		for (Declaration d : declarations)
			sortedDeclarations.add(BrailleCssSerializer.toString(d));
		Collections.sort(sortedDeclarations);
		StringBuilder s = new StringBuilder();
		Iterator it = sortedDeclarations.iterator();
		while (it.hasNext()) {
			s.append(it.next());
			if (it.hasNext()) s.append(separator);
		}
		return s.toString();
	}

	/* = PRIVATE ========================================= */

	private static String toString(URL url) {
		if (url.url != null)
			return toString(url.url);
		else
			return toString(url.urlAttr);
	}

	private static String toString(CounterStyle style) {
		if (style.name != null)
			return style.name;
		else if (style.symbol != null)
			return toString(style.symbol);
		else if (style.symbols != null)
			return toString(style.symbols);
		else
			return "none";
	}

	/* =================================================== */
	/* toAttributes                                        */
	/* =================================================== */

	public static void toAttributes(BrailleCssStyle style, XMLStreamWriter writer) throws XMLStreamException {
		if (style.nestedStyles != null)
			throw new UnsupportedOperationException();
		if (style.declarations != null)
			for (Declaration d : style.declarations)
				toAttribute(d, writer);
	}

	/**
	 * @param relativeTo If not {@code null}, include only those declarations that are needed
	 *                   to reconstruct {@code style} with {@code relativeTo} as the parent
	 *                   style. Relativizes even if the parent style is empty.
	 */
	public static void toAttributes(BrailleCssStyle style, BrailleCssStyle relativeTo, XMLStreamWriter writer)
			throws XMLStreamException {
		if (relativeTo == null)
			toAttributes(style, writer);
		else
			toAttributes(style.relativize(relativeTo), writer);
	}

	private static void toAttribute(Declaration declaration, XMLStreamWriter writer) throws XMLStreamException {
		writeAttribute(writer,
		               new QName(XMLNS_CSS, declaration.getProperty().replaceAll("^-", "_"), "css"),
		               declaration instanceof PropertyValue
		                   ? serializePropertyValue((PropertyValue)declaration)
		                   : serializeTermList(declaration));
	}

	/* =================================================== */
	/* toXml                                               */
	/* =================================================== */

	private static final String XMLNS_CSS       = "http://www.daisy.org/ns/pipeline/braille-css";
	private static final QName CSS_RULE         = new QName(XMLNS_CSS, "rule",     "css");
	private static final QName CSS_PROPERTY     = new QName(XMLNS_CSS, "property", "css");
	private static final QName CSS_STRING       = new QName(XMLNS_CSS, "string",   "css");
	private static final QName CSS_CONTENT      = new QName(XMLNS_CSS, "content",  "css");
	private static final QName CSS_ATTR         = new QName(XMLNS_CSS, "attr",     "css");
	private static final QName CSS_COUNTER      = new QName(XMLNS_CSS, "counter",  "css");
	private static final QName CSS_TEXT         = new QName(XMLNS_CSS, "text",     "css");
	private static final QName CSS_LEADER       = new QName(XMLNS_CSS, "leader",   "css");
	private static final QName CSS_FLOW         = new QName(XMLNS_CSS, "flow", "css");
	private static final QName CSS_CUSTOM_FUNC  = new QName(XMLNS_CSS, "custom-func", "css");
	private static final QName CSS_STRING_SET   = new QName(XMLNS_CSS, "string-set", "css");
	private static final QName CSS_COUNTER_SET  = new QName(XMLNS_CSS, "counter-set", "css");
	private static final QName SELECTOR         = new QName("selector");
	private static final QName NAME             = new QName("name");
	private static final QName VALUE            = new QName("value");
	private static final QName STYLE            = new QName("style");
	private static final QName TARGET           = new QName("target");
	private static final QName TARGET_ATTRIBUTE = new QName("target-attribute");
	private static final QName SCOPE            = new QName("scope");
	private static final QName PATTERN          = new QName("pattern");
	private static final QName POSITION         = new QName("position");
	private static final QName ALIGNMENT        = new QName("alignment");
	private static final QName FROM             = new QName("from");

	public static void toXml(BrailleCssStyle style, XMLStreamWriter writer) throws XMLStreamException {
		toXml(style, writer, false);
	}

	private static void toXml(BrailleCssStyle style,
	                          XMLStreamWriter w,
	                          boolean recursive) throws XMLStreamException {
		if (style.declarations != null && !Iterables.isEmpty(style.declarations)) {
			if (!recursive || style.nestedStyles != null)
				writeStartElement(w, CSS_RULE);
			List declarations = new ArrayList<>();
			for (Declaration d : style.declarations) declarations.add(d);
			Collections.sort(declarations, Ordering.natural().onResultOf(Declaration::getProperty));
			for (Declaration d : declarations)
				toXml(d, w);
			if (!recursive || style.nestedStyles != null)
				w.writeEndElement();
		}
		if (style.nestedStyles != null)
			for (Map.Entry e : style.nestedStyles.entrySet()) {
				writeStartElement(w, CSS_RULE);
				writeAttribute(w, SELECTOR, e.getKey());
				toXml(e.getValue(), w, true);
				w.writeEndElement();
			}
	}

	public static void toXml(Declaration declaration, XMLStreamWriter writer) throws XMLStreamException {
		if (declaration instanceof PropertyValue)
			toXml((PropertyValue)declaration, writer);
		else {
			writeStartElement(writer, CSS_PROPERTY);
			writeAttribute(writer, NAME, declaration.getProperty());
			writeAttribute(writer, VALUE, serializeTermList(declaration));
			writer.writeEndElement();
		}
	}

	public static void toXml(PropertyValue value, XMLStreamWriter writer) throws XMLStreamException {
		writeStartElement(writer, CSS_PROPERTY);
		writeAttribute(writer, NAME, value.getProperty());
		CSSProperty p = value.getCSSProperty();
		if (p == Content.NONE || p == Content.INITIAL)
			;
		else if (p == Content.INHERIT)
			writeAttribute(writer, VALUE, Content.INHERIT.toString());
		else if (p == Content.content_list) {
			if (value.getValue() instanceof ContentList)
				toXml((ContentList)value.getValue(), writer);
			else
				throw new IllegalArgumentException();
		} else if (p == StringSet.NONE || p == StringSet.INITIAL)
			;
		else if (p == StringSet.INHERIT)
			writeAttribute(writer, VALUE, StringSet.INHERIT.toString());
		else if (p == StringSet.list_values)
			if (value.getValue() instanceof StringSetList)
				toXml((StringSetList)value.getValue(), writer);
			else
				throw new IllegalArgumentException();
		else if (p == CounterSet.NONE || p == CounterSet.INITIAL ||
		         p == CounterReset.NONE || p == CounterReset.INITIAL ||
		         p == CounterIncrement.NONE || p == CounterIncrement.INITIAL)
			;
		else if (p == CounterSet.INHERIT ||
		         p == CounterReset.INHERIT ||
		         p == CounterIncrement.INHERIT)
			writeAttribute(writer, VALUE, "inherit");
		else if (p == CounterSet.list_values ||
		         p == CounterReset.list_values ||
		         p == CounterIncrement.list_values)
			if (value.getValue() instanceof CounterSetList)
				toXml((CounterSetList)value.getValue(), writer);
			else
				throw new IllegalArgumentException();
		else
			writeAttribute(writer, VALUE, serializePropertyValue(value));
		writer.writeEndElement();
	}

	public static void toXml(ContentList list, XMLStreamWriter w) throws XMLStreamException {
		for (Term i : list)
			if (i instanceof TermString || i instanceof TermURI) {
				Term s = (Term)i;
				writeStartElement(w, CSS_STRING);
				writeAttribute(w, VALUE, s.getValue());
				w.writeEndElement(); }
			else if (i instanceof AttrFunction) {
				AttrFunction f = (AttrFunction)i;
				writeStartElement(w, CSS_ATTR);
				writeAttribute(w, NAME, f.name.getValue());
				w.writeEndElement(); }
			else if (i instanceof ContentFunction) {
				ContentFunction f = (ContentFunction)i;
				writeStartElement(w, CSS_CONTENT);
				if (f.target.isPresent())
					if (f.target.get().url != null) {
						TermURI t = f.target.get().url;
						URI url = URLs.asURI(t.getValue());
						if (t.getBase() != null)
							url = URLs.resolve(URLs.asURI(t.getBase()), url);
						writeAttribute(w, TARGET, url.toString());
					} else
						writeAttribute(w, TARGET_ATTRIBUTE, f.target.get().urlAttr.name.getValue());
				w.writeEndElement(); }
			else if (i instanceof StringFunction) {
				StringFunction f = (StringFunction)i;
				writeStartElement(w, CSS_STRING);
				writeAttribute(w, NAME, f.name.getValue());
				if (f.target.isPresent())
					if (f.target.get().url != null) {
						TermURI t = f.target.get().url;
						URI url = URLs.asURI(t.getValue());
						if (t.getBase() != null)
							url = URLs.resolve(URLs.asURI(t.getBase()), url);
						writeAttribute(w, TARGET, url.toString());
					} else
						writeAttribute(w, TARGET_ATTRIBUTE, f.target.get().urlAttr.name.getValue());
				else if (f.scope.isPresent())
					writeAttribute(w, SCOPE, f.scope.get().toString());
				w.writeEndElement(); }
			else if (i instanceof CounterFunction) {
				CounterFunction f = (CounterFunction)i;
				writeStartElement(w, CSS_COUNTER);
				writeAttribute(w, NAME, f.name.getValue());
				if (f.target.isPresent())
					if (f.target.get().url != null) {
						TermURI t = f.target.get().url;
						URI url = URLs.asURI(t.getValue());
						if (t.getBase() != null)
							url = URLs.resolve(URLs.asURI(t.getBase()), url);
						writeAttribute(w, TARGET, url.toString());
					} else
						writeAttribute(w, TARGET_ATTRIBUTE, f.target.get().urlAttr.name.getValue());
				if (f.style.isPresent())
					writeAttribute(w, STYLE, toString(f.style.get()));
				w.writeEndElement(); }
			else if (i instanceof TextFunction) {
				TextFunction f = (TextFunction)i;
				writeStartElement(w, CSS_TEXT);
				if (f.target.url != null) {
					TermURI t = f.target.url;
					URI url = URLs.asURI(t.getValue());
					if (t.getBase() != null)
						url = URLs.resolve(URLs.asURI(t.getBase()), url);
					writeAttribute(w, TARGET, url.toString());
				} else
					writeAttribute(w, TARGET_ATTRIBUTE, f.target.urlAttr.name.getValue());
				w.writeEndElement(); }
			else if (i instanceof LeaderFunction) {
				LeaderFunction f = (LeaderFunction)i;
				writeStartElement(w, CSS_LEADER);
				writeAttribute(w, PATTERN, f.pattern.getValue());
				if (f.position.isPresent())
					writeAttribute(w, POSITION, toString(f.position.get()));
				if (f.alignment.isPresent())
					writeAttribute(w, ALIGNMENT, f.alignment.get().toString());
				w.writeEndElement(); }
			else if (i instanceof FlowFunction) {
				FlowFunction f = (FlowFunction)i;
				writeStartElement(w, CSS_FLOW);
				writeAttribute(w, FROM, f.from.getValue());
				if (f.scope.isPresent())
					writeAttribute(w, SCOPE, f.scope.get().toString());
				w.writeEndElement(); }
			else if (i instanceof TermFunction) {
				TermFunction f = (TermFunction)i;
				writeStartElement(w, CSS_CUSTOM_FUNC);
				writeAttribute(w, NAME, f.getFunctionName());
				int k = 0;
				for (Term arg : f) {
					k++;
					writeAttribute(w, new QName("arg" + k), toString(arg)); }
				w.writeEndElement(); }
			else
				throw new RuntimeException("coding error");
	}

	public static void toXml(StringSetList list, XMLStreamWriter w) throws XMLStreamException {
		for (Term i : list) {
			if (i instanceof StringSetList.StringSet) {
				StringSetList.StringSet s = (StringSetList.StringSet)i;
				writeStartElement(w, CSS_STRING_SET);
				writeAttribute(w, NAME, s.getKey());
				toXml(s.getValue(), w);
				w.writeEndElement(); }
			else
				throw new RuntimeException("coding error");
		}
	}

	public static void toXml(CounterSetList list, XMLStreamWriter w) throws XMLStreamException {
		for (Term i : list) {
			if (i instanceof CounterSetList.CounterSet) {
				CounterSetList.CounterSet s = (CounterSetList.CounterSet)i;
				writeStartElement(w, CSS_COUNTER_SET);
				writeAttribute(w, NAME, s.getKey());
				writeAttribute(w, VALUE, "" + s.getValue());
				w.writeEndElement(); }
			else
				throw new RuntimeException("coding error");
		}
	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy