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

org.opencms.flex.CmsFlexCacheKey Maven / Gradle / Ivy

Go to download

OpenCms is an enterprise-ready, easy to use website content management system based on Java and XML technology. Offering a complete set of features, OpenCms helps content managers worldwide to create and maintain beautiful websites fast and efficiently.

There is a newer version: 17.0
Show newest version
/*
 * This library is part of OpenCms -
 * the Open Source Content Management System
 *
 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * For further information about Alkacon Software GmbH & Co. KG, please see the
 * company website: http://www.alkacon.com
 *
 * For further information about OpenCms, please see the
 * project website: http://www.opencms.org
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package org.opencms.flex;

import org.opencms.flex.CmsFlexRequestKey.PathsBean;
import org.opencms.loader.I_CmsResourceLoader;
import org.opencms.main.CmsLog;
import org.opencms.util.CmsStringUtil;

import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;

import com.google.common.collect.Lists;

/**
 * Key used to describe the caching behaviour of a specific resource.

* * It has a lot of variables that are directly accessed (which isn't good style, I know) * to avoid method calling overhead (a cache is about speed, isn't it :).

* * @since 6.0.0 */ public class CmsFlexCacheKey { /** Flex cache keyword: always. */ private static final String CACHE_00_ALWAYS = "always"; /** Flex cache keyword: never. */ private static final String CACHE_01_NEVER = "never"; /** Flex cache keyword: uri. */ private static final String CACHE_02_URI = "uri"; /** Flex cache keyword: user. */ private static final String CACHE_03_USER = "user"; /** Flex cache keyword: params. */ private static final String CACHE_04_PARAMS = "params"; /** Flex cache keyword: no-params. */ private static final String CACHE_05_NO_PARAMS = "no-params"; /** Flex cache keyword: timeout. */ private static final String CACHE_06_TIMEOUT = "timeout"; /** Flex cache keyword: session. */ private static final String CACHE_07_SESSION = "session"; /** Flex cache keyword: schemes. */ private static final String CACHE_08_SCHEMES = "schemes"; /** Flex cache keyword: ports. */ private static final String CACHE_09_PORTS = "ports"; /** Flex cache keyword: false. */ private static final String CACHE_10_FALSE = CmsStringUtil.FALSE; /** Flex cache keyword: parse-error. */ private static final String CACHE_11_PARSE_ERROR = "parse-error"; /** Flex cache keyword: true. */ private static final String CACHE_12_TRUE = CmsStringUtil.TRUE; /** Flex cache keyword: ip. */ private static final String CACHE_13_IP = "ip"; /** Flex cache keyword: element. */ private static final String CACHE_14_ELEMENT = "element"; /** Flex cache keyword: locale. */ private static final String CACHE_15_LOCALE = "locale"; /** Flex cache keyword: encoding. */ private static final String CACHE_16_ENCODING = "encoding"; /** Flex cache keyword: site. */ private static final String CACHE_17_SITE = "site"; /** Flex cache keyword: attrs. */ private static final String CACHE_18_ATTRS = "attrs"; /** Flex cache keyword: no-attrs. */ private static final String CACHE_19_NO_ATTRS = "no-attrs"; /** Flex cache keyword: device. */ private static final String CACHE_20_DEVICE = "device"; /** Flex cache keyword: container-element. */ private static final String CACHE_21_CONTAINER_ELEMENT = "container-element"; /** Flex cache keyword: ignore. */ private static final String CACHE_22_IGNORE = "ignore"; /** Flex cache key component for the __forceAbsoluteLinks parameter. */ private static final String CACHE_FORCE_ABSOLUTE_LINKS = "force-abs"; /** The list of keywords of the Flex cache language. */ private static final List CACHE_COMMANDS = Arrays.asList( new String[] { CACHE_00_ALWAYS, CACHE_01_NEVER, CACHE_02_URI, CACHE_03_USER, CACHE_04_PARAMS, CACHE_05_NO_PARAMS, CACHE_06_TIMEOUT, CACHE_07_SESSION, CACHE_08_SCHEMES, CACHE_09_PORTS, CACHE_10_FALSE, CACHE_11_PARSE_ERROR, CACHE_12_TRUE, CACHE_13_IP, CACHE_14_ELEMENT, CACHE_15_LOCALE, CACHE_16_ENCODING, CACHE_17_SITE, CACHE_18_ATTRS, CACHE_19_NO_ATTRS, CACHE_20_DEVICE, CACHE_21_CONTAINER_ELEMENT, CACHE_22_IGNORE}); /** Marker to identify use of certain String key members (uri, ip etc.). */ private static final String IS_USED = "/ /"; /** The log object for this class. */ private static final Log LOG = CmsLog.getLog(CmsFlexCacheKey.class); /** Cache key variable: Determines if this resource can be cached alwys, never or under certain conditions. -1 = never, 0=check, 1=always. */ private int m_always; /** Cache key variable: List of attributes. */ private Set m_attrs; /** Cache key variable: The current container element. */ private String m_containerElement; /** Cache key variable: The current device. */ private String m_device; /** Cache key variable: The requested element. */ private String m_element; /** Cache key variable: The requested encoding. */ private String m_encoding; /** Cache key variable: The ip address of the request. */ private String m_ip; /** Cache key variable: The requested locale. */ private String m_locale; /** Cache key variable: List of "blocking" attributes. */ private Set m_noattrs; /** Cache key variable: List of "blocking" parameters. */ private Set m_noparams; /** Cache key variable: List of parameters. */ private Set m_params; /** Flag raised in case a key parse error occurred. */ private boolean m_parseError; /** Cache key variable: The request TCP/IP port. */ private Set m_ports; /** The OpenCms resource that this key is used for. */ private String m_resource; /** Cache key variable: Distinguishes request schemes (http, https etc.). */ private Set m_schemes; /** Cache key variable: List of session variables. */ private Set m_session; /** Cache key variable: The current site root. */ private String m_site; /** Cache key variable: Timeout of the resource. */ private long m_timeout; /** Cache key variable: The uri of the original request. */ private String m_uri; /** Cache key variable: The user id. */ private String m_user; /** Resource without online / offline suffix. */ private String m_actualResource; /** True if 'ignore' directive is set. */ private boolean m_ignore; /** * This constructor is used when building a cache key from set of cache directives.

* * These directives are attached to the properties of the requested resource * on a property called "cache". * The value of this poperty that is passed in this constructor as "cacheDirectives" * is parsed to build the keys data structure.

* * In case a parsing error occures, the value of this key is set to "cache=never", * and the hadParseError() flag is set to true. * This is done to ensure that a valid key is always constructed with the constructor.

* * @param resourcename the full name of the resource including site root * @param cacheDirectives the cache directives of the resource (value of the property "cache") * @param online must be true for an online resource, false for offline resources */ public CmsFlexCacheKey(String resourcename, String cacheDirectives, boolean online) { m_actualResource = resourcename; m_resource = getKeyName(resourcename, online); m_always = -1; m_timeout = -1; if (cacheDirectives != null) { parseFlexKey(cacheDirectives); } if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_GENERATED_1, toString())); } } /** * Calculates the cache key name that is used as key in * the first level of the FlexCache.

* * @param resourcename the full name of the resource including site root * @param online must be true for an online resource, false for offline resources * * @return the FlexCache key name */ public static String getKeyName(String resourcename, boolean online) { return resourcename.concat(online ? CmsFlexCache.CACHE_ONLINESUFFIX : CmsFlexCache.CACHE_OFFLINESUFFIX); } /** * Returns resource name from given key name.

* * @param keyName given name of key. * @return name of resource if key is valid, otherwise "" */ public static String getResourceName(String keyName) { if (keyName.endsWith(CmsFlexCache.CACHE_OFFLINESUFFIX) | keyName.endsWith(CmsFlexCache.CACHE_ONLINESUFFIX)) { return keyName.split(" ")[0]; } else { return ""; } } /** * Appends a flex cache key value to the given buffer.

* * @param str the buffer to append to * @param key the key to append * @param value the value to append */ private static void appendKeyValue(StringBuffer str, String key, String value) { str.append(key); if (value == IS_USED) { str.append(";"); } else { str.append("=("); str.append(value); str.append(");"); } } /** * Returns the actual resource path under which this is cached, without online / offline suffix.

* * @return the actual resource path */ public String getActualResource() { return m_actualResource; } /** * Gets the list of root paths for the cache key / request key combination which should be used to determine the set of flex cache buckets for the flex cache entry.

* * @param key the flex request key * @return the list of paths which should be used to determine the flex cache buckets */ public List getPathsForBuckets(CmsFlexRequestKey key) { PathsBean pathBean = key.getPaths(); List paths = Lists.newArrayList(); if (m_uri != null) { paths.add(pathBean.getUri()); paths.add(pathBean.getDetailElement()); } if (m_site != null) { paths.add(pathBean.getSite()); } if (m_containerElement != null) { paths.add(pathBean.getContainerElement()); } paths.removeAll(Collections.singletonList(null)); return paths; } /** * This flag is used to indicate that a parse error had * occurred, which can happen if the cache directives String * passed to the constructor using the response is * not build according to the Flex cache language syntax.

* * @return true if a parse error did occur, false otherwise */ public boolean hadParseError() { return m_parseError; } /** * Returns true if 'ignore' directive is set.

* * Mostly the same as 'never', but prevents the 'Cache-Control: public, max-age=0' header from being set * * @return true if 'ignore' is set */ public boolean isIgnore() { return m_ignore; } /** * Compares this key to the other key passed as parameter, * from comparing the two keys, a variation String is constructed.

* * This method is the "heart" of the key matching process.

* * The assumtion is that this key should be the one constructed for the response, * while the parameter key should have been constructed from the request.

* * A short example how this works: * If the cache key is "cache=user" and the request is done from a guest user * the constructed variation will be "user=(guest)".

* * @param key the key to match this key with * @return null if not cachable, or the Variation String if cachable */ public String matchRequestKey(CmsFlexRequestKey key) { StringBuffer str = new StringBuffer(100); if (m_always < 0) { if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_KEYMATCH_CACHE_NEVER_0)); } return null; } if (m_ignore) { LOG.debug("Not matching because 'ignore' directive is set."); return null; } if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_KEYMATCH_CHECK_NO_PARAMS_0)); } if ((m_noparams != null) && (key.getParams() != null)) { if ((m_noparams.size() == 0) && (key.getParams().size() > 0)) { return null; } Iterator i = key.getParams().keySet().iterator(); while (i.hasNext()) { if (m_noparams.contains(i.next())) { return null; } } } if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_KEYMATCH_CHECK_NO_ATTRS_0)); } if ((m_noattrs != null) && (key.getAttributes() != null)) { if ((m_noattrs.size() == 0) && (key.getAttributes().size() > 0)) { return null; } Iterator i = key.getAttributes().keySet().iterator(); while (i.hasNext()) { if (m_noattrs.contains(i.next())) { return null; } } } if (m_always > 0) { if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_KEYMATCH_CACHE_ALWAYS_0)); } str.append(CACHE_00_ALWAYS); return str.toString(); } if (m_uri != null) { appendKeyValue(str, CACHE_02_URI, key.getUri()); } if (m_site != null) { appendKeyValue(str, CACHE_17_SITE, key.getSite()); } if (m_element != null) { appendKeyValue(str, CACHE_14_ELEMENT, key.getElement()); } if (m_device != null) { appendKeyValue(str, CACHE_20_DEVICE, key.getDevice()); } if (m_containerElement != null) { appendKeyValue(str, CACHE_21_CONTAINER_ELEMENT, key.getContainerElement()); } if (m_locale != null) { appendKeyValue(str, CACHE_15_LOCALE, key.getLocale()); } if (m_encoding != null) { appendKeyValue(str, CACHE_16_ENCODING, key.getEncoding()); } if (m_ip != null) { appendKeyValue(str, CACHE_13_IP, key.getIp()); } if (m_user != null) { appendKeyValue(str, CACHE_03_USER, key.getUser()); } if (m_params != null) { str.append(CACHE_04_PARAMS); str.append("=("); Map keyParams = key.getParams(); if (keyParams != null) { if (m_params.size() > 0) { // match only params listed in cache directives Iterator i = m_params.iterator(); while (i.hasNext()) { Object o = i.next(); if (keyParams.containsKey(o)) { str.append(o); str.append("="); // TODO: handle multiple occurrences of the same parameter value String[] values = keyParams.get(o); str.append(values[0]); if (i.hasNext()) { str.append(","); } } } } else { // match all request params Iterator> i = keyParams.entrySet().iterator(); while (i.hasNext()) { Map.Entry entry = i.next(); str.append(entry.getKey()); str.append("="); // TODO: handle multiple occurrences of the same parameter value String[] values = entry.getValue(); str.append(values[0]); if (i.hasNext()) { str.append(","); } } } } str.append(");"); } if (m_attrs != null) { str.append(CACHE_18_ATTRS); str.append("=("); Map keyAttrs = key.getAttributes(); if (keyAttrs != null) { if (m_attrs.size() > 0) { // match only attributes listed in cache directives Iterator i = m_attrs.iterator(); while (i.hasNext()) { String s = i.next(); if (keyAttrs.containsKey(s)) { str.append(s); str.append("="); Object value = keyAttrs.get(s); str.append(value); if (i.hasNext()) { str.append(","); } } } } else { // match all request attributes Iterator> i = keyAttrs.entrySet().iterator(); while (i.hasNext()) { Map.Entry entry = i.next(); str.append(entry.getKey()); str.append("="); Object value = entry.getValue(); str.append(value); if (i.hasNext()) { str.append(","); } } } } str.append(");"); } if (m_session != null) { StringBuffer buf = new StringBuffer(32); boolean found = false; buf.append(CACHE_07_SESSION); buf.append("=("); HttpSession keySession = key.getSession(); if (keySession != null) { // match only session attributes listed in cache directives Iterator i = m_session.iterator(); while (i.hasNext()) { String name = i.next(); Object val = keySession.getAttribute(name); if (val != null) { found = true; buf.append(name); buf.append("="); buf.append(val); if (i.hasNext()) { buf.append(","); } } } } if (found) { buf.append(");"); str.append(buf); } } if (m_schemes != null) { String s = key.getScheme(); if ((m_schemes.size() > 0) && (!m_schemes.contains(s))) { return null; } appendKeyValue(str, CACHE_08_SCHEMES, s); } if (m_ports != null) { Integer i = key.getPort(); if ((m_ports.size() > 0) && (!m_ports.contains(i))) { return null; } str.append(CACHE_09_PORTS); str.append("=("); str.append(i); str.append(");"); } if (m_timeout > 0) { str.append(CACHE_06_TIMEOUT); str.append("=("); str.append(m_timeout); str.append(");"); } if (str.length() > 0) { // we don't want an element to just be cached with the __forceAbsoluteLinks parameter as key if it wouldn't be cached otherwise appendKeyValue(str, CACHE_FORCE_ABSOLUTE_LINKS, "" + key.isForceAbsoluteLinks()); return str.toString(); } else { return null; } } /** * @see java.lang.Object#toString() * * @return a complete String representation for this key */ @Override public String toString() { StringBuffer str = new StringBuffer(100); if (m_always < 0) { str.append(CACHE_01_NEVER); if (m_parseError) { str.append(";"); str.append(CACHE_11_PARSE_ERROR); } return str.toString(); } if (m_ignore) { // return "ignore" str.append(CACHE_22_IGNORE); return str.toString(); } if (m_noparams != null) { // add "no-cachable" parameters str.append(CACHE_05_NO_PARAMS); if (m_noparams.size() == 0) { str.append(";"); } else { str.append("=("); Iterator i = m_noparams.iterator(); while (i.hasNext()) { Object o = i.next(); str.append(o); if (i.hasNext()) { str.append(","); } } str.append(");"); } } if (m_noattrs != null) { // add "no-cachable" attributes str.append(CACHE_19_NO_ATTRS); if (m_noattrs.size() == 0) { str.append(";"); } else { str.append("=("); Iterator i = m_noattrs.iterator(); while (i.hasNext()) { String s = i.next(); str.append(s); if (i.hasNext()) { str.append(","); } } str.append(");"); } } if (m_always > 0) { str.append(CACHE_00_ALWAYS); if (m_parseError) { str.append(";"); str.append(CACHE_11_PARSE_ERROR); } return str.toString(); } if (m_uri != null) { // add uri appendKeyValue(str, CACHE_02_URI, m_uri); } if (m_site != null) { // add site appendKeyValue(str, CACHE_17_SITE, m_site); } if (m_element != null) { // add element appendKeyValue(str, CACHE_14_ELEMENT, m_element); } if (m_device != null) { appendKeyValue(str, CACHE_20_DEVICE, m_device); } if (m_containerElement != null) { appendKeyValue(str, CACHE_21_CONTAINER_ELEMENT, m_containerElement); } if (m_locale != null) { // add locale appendKeyValue(str, CACHE_15_LOCALE, m_locale); } if (m_encoding != null) { // add encoding appendKeyValue(str, CACHE_16_ENCODING, m_encoding); } if (m_ip != null) { // add ip appendKeyValue(str, CACHE_13_IP, m_ip); } if (m_user != null) { // add user appendKeyValue(str, CACHE_03_USER, m_user); } if (m_params != null) { // add parameters str.append(CACHE_04_PARAMS); if (m_params.size() == 0) { str.append(";"); } else { str.append("=("); Iterator i = m_params.iterator(); while (i.hasNext()) { Object o = i.next(); if (I_CmsResourceLoader.PARAMETER_ELEMENT.equals(o)) { continue; } str.append(o); if (i.hasNext()) { str.append(","); } } str.append(");"); } } if (m_attrs != null) { // add attributes str.append(CACHE_18_ATTRS); if (m_attrs.size() == 0) { str.append(";"); } else { str.append("=("); Iterator i = m_attrs.iterator(); while (i.hasNext()) { String s = i.next(); str.append(s); if (i.hasNext()) { str.append(","); } } str.append(");"); } } if (m_session != null) { // add session variables str.append(CACHE_07_SESSION); str.append("=("); Iterator i = m_session.iterator(); while (i.hasNext()) { Object o = i.next(); str.append(o); if (i.hasNext()) { str.append(","); } } str.append(");"); } if (m_timeout >= 0) { // add timeout str.append(CACHE_06_TIMEOUT); str.append("=("); str.append(m_timeout); str.append(");"); } if (m_schemes != null) { // add schemes str.append(CACHE_08_SCHEMES); if (m_schemes.size() == 0) { str.append(";"); } else { str.append("=("); Iterator i = m_schemes.iterator(); while (i.hasNext()) { str.append(i.next()); if (i.hasNext()) { str.append(","); } } str.append(");"); } } if (m_ports != null) { // add ports str.append(CACHE_09_PORTS); if (m_ports.size() == 0) { str.append(";"); } else { str.append("=("); Iterator i = m_ports.iterator(); while (i.hasNext()) { str.append(i.next()); if (i.hasNext()) { str.append(","); } } str.append(");"); } } if (m_parseError) { str.append(CACHE_11_PARSE_ERROR); } return str.toString(); } /** * Returns the resource.

* * @return the resource */ protected String getResource() { return m_resource; } /** * Returns the timeout.

* * @return the timeout */ protected long getTimeout() { return m_timeout; } /** * Parse a String in the Flex cache language and construct * the key data structure from this.

* * @param key the String to parse (usually read from the file property "cache") */ private void parseFlexKey(String key) { List tokens = CmsStringUtil.splitAsList(key, ';', false); Iterator i = tokens.iterator(); try { while (i.hasNext()) { String t = i.next(); String k = null; String v = null; int idx = t.indexOf('='); if (idx >= 0) { k = t.substring(0, idx).trim(); if (t.length() > idx) { v = t.substring(idx + 1).trim(); } } else { k = t.trim(); } m_always = 0; if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_PARSE_FLEXKEY_3, t, k, v)); } switch (CACHE_COMMANDS.indexOf(k)) { case 0: // always case 12: // true m_always = 1; // continue processing (make sure we find a "never" behind "always") break; case 1: // never case 10: // false m_always = -1; // no need for any further processing return; case 2: // uri m_uri = IS_USED; // marks m_uri as being used break; case 3: // user m_user = IS_USED; // marks m_user as being used break; case 4: // params if (v != null) { m_params = parseValueList(v); } else { m_params = Collections.emptySet(); } if (m_params.contains(I_CmsResourceLoader.PARAMETER_ELEMENT)) { // workaround for element setting by parameter in OpenCms < 6.0 m_element = IS_USED; m_params.remove(I_CmsResourceLoader.PARAMETER_ELEMENT); if (m_params.size() == 0) { m_params = null; } } break; case 5: // no-params if (v != null) { // no-params are present m_noparams = parseValueList(v); } else { // never cache with parameters m_noparams = Collections.emptySet(); } break; case 6: // timeout m_timeout = Integer.parseInt(v); break; case 7: // session m_session = parseValueList(v); if (m_session.size() <= 0) { // session must have at last one variable set m_parseError = true; } break; case 8: // schemes m_schemes = parseValueList(v); break; case 9: // ports Set ports = parseValueList(v); m_ports = new HashSet(ports.size()); for (String p : ports) { try { m_ports.add(Integer.valueOf(p)); } catch (NumberFormatException e) { // ignore this number } } break; case 11: // previous parse error - ignore break; case 13: // ip m_ip = IS_USED; // marks ip as being used break; case 14: // element m_element = IS_USED; break; case 15: // locale m_locale = IS_USED; break; case 16: // encoding m_encoding = IS_USED; break; case 17: // site m_site = IS_USED; break; case 18: // attrs if (v != null) { m_attrs = parseValueList(v); } else { m_attrs = null; } break; case 19: // no-attrs if (v != null) { // no-attrs are present m_noattrs = parseValueList(v); } else { // never cache with attributes m_noattrs = Collections.emptySet(); } break; case 20: // device m_device = IS_USED; // marks m_device as being used break; case 21: // container element m_containerElement = IS_USED; break; case 22: m_ignore = true; break; default: // unknown directive, throw error m_parseError = true; } } } catch (Exception e) { // any Exception here indicates a parsing error if (LOG.isErrorEnabled()) { LOG.error(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_PARSE_ERROR_1, e.toString()), e); } m_parseError = true; } if (m_parseError) { // If string is invalid set cache to "never" m_always = -1; } } /** * A helper method for the parsing process which parses * Strings like groups=(a, b, c).

* * @param value the String to parse * @return a Map that contains of the parsed values, only the keyset of the Map is needed later */ private Set parseValueList(String value) { if (value.charAt(0) == '(') { value = value.substring(1); } int len = value.length() - 1; if (value.charAt(len) == ')') { value = value.substring(0, len); } if (value.charAt(len - 1) == ',') { value = value.substring(0, len - 1); } if (LOG.isDebugEnabled()) { LOG.debug(Messages.get().getBundle().key(Messages.LOG_FLEXCACHEKEY_PARSE_VALUES_1, value)); } List tokens = CmsStringUtil.splitAsList(value, ',', true); Set result = new HashSet(); result.addAll(tokens); return result; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy