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

org.apache.royale.compiler.internal.mxml.MXMLData Maven / Gradle / Ivy

There is a newer version: 0.9.10
Show newest version
/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 org.apache.royale.compiler.internal.mxml;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;

import org.apache.commons.io.IOUtils;

import org.apache.royale.compiler.common.MutablePrefixMap;
import org.apache.royale.compiler.common.PrefixMap;
import org.apache.royale.compiler.filespecs.FileSpecification;
import org.apache.royale.compiler.filespecs.IFileSpecification;
import org.apache.royale.compiler.internal.parsing.mxml.BalancingMXMLProcessor;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLToken;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLTokenizer;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLUnitDataIterator;
import org.apache.royale.compiler.mxml.IMXMLData;
import org.apache.royale.compiler.mxml.IMXMLTagAttributeData;
import org.apache.royale.compiler.mxml.IMXMLTagData;
import org.apache.royale.compiler.mxml.IMXMLUnitData;
import org.apache.royale.compiler.parsing.MXMLTokenTypes;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.SyntaxProblem;
import org.apache.royale.utils.FastStack;
import org.apache.royale.utils.FastStack.IFastStackDecorator;

/**
 * Encapsulation of an MXML file, with individual units for each open tag, close
 * tag, and block of text.
 */
public class MXMLData implements IMXMLData
{

    private final class TokenizerPayload
    {
        private List tokens;

        private PrefixMap prefixMap;

        public TokenizerPayload(List tokens, PrefixMap prefixMap)
        {
            this.tokens = tokens;
            this.prefixMap = prefixMap;
        }

        public List getMXMLTokens()
        {
            return tokens;
        }

        public PrefixMap getPrefixMap()
        {
            return prefixMap;
        }
    }

    /**
     * A cursor represents the offset into the document and the index of the
     * MXML unit that contains that offset
     */
    public class Cursor
    {
        /**
         * Offset into the document
         */
        private int offset;
        /**
         * Index of the unit that contains that offset
         */
        private int unitIndex;

        /**
         * Constructor (points to beginning of the MXMLData)
         */
        public Cursor()
        {
            reset();
        }

        /**
         * Reset cursor to the beginning of the MXMLData
         */
        public void reset()
        {
            offset = 0;
            unitIndex = 0;
        }

        /**
         * Set the cursor to a particular offset/unit
         * 
         * @param offset offset into the document
         * @param unitIndex index of the unit containing that offset
         */
        public void setCursor(int offset, int unitIndex)
        {
            this.offset = offset;
            this.unitIndex = unitIndex;
        }

        /**
         * Get the offset the cursor is pointing to
         * 
         * @return current document offset
         */
        public int getOffset()
        {
            return offset;
        }

        /**
         * Get the index of the unit the cursor is pointing to
         * 
         * @return current unit index
         */
        public int getUnitIndex()
        {
            return unitIndex;
        }
    }

    private String path = null;

    /**
     * Individual units for each open tag, close tag, and block of text
     */
    private IMXMLUnitData[] units;

    /**
     * Flag indicating that the tokens underlying this structure were fixed
     */
    private boolean wasRepaired;

    private boolean shouldRepair = true;

    /**
     * This maps {@link IMXMLTagData} objects to their explicit
     * {@link PrefixMap} if it exist
     */
    private HashMap nsMap;

    /**
     * The cursor holds the result of last offset lookup into the MXMLData (see
     * findNearestUnit, below). When an edit happens that causes the code
     * coloring engine to recompute everything from the edit position to the end
     * of the document, it will request MXMLOffsetInformations in order. Having
     * the cursor speeds this up tremendously.
     */
    private Cursor cursor;

    /**
     * The dialect of MXML being used.
     */
    private MXMLDialect mxmlDialect;

    private Collection problems = new ArrayList(2);

    /**
     * Tells us if we are currently processing full MXML, or a fragment
     */
    private boolean fullContent = true;

    /**
     * Constructor
     * 
     * @param tokens MXML tokens to build the MXMLData object from
     * @param map the {@link PrefixMap} for the document, containing the
     * namespace/prefix mappings
     */
    public MXMLData(List tokens, PrefixMap map, IFileSpecification fileSpec)
    {
        path = fileSpec.getPath();
        init(tokens, map);
    }

    public MXMLData(List tokens, PrefixMap map, IFileSpecification fileSpec, boolean shouldRepair)
    {
        path = fileSpec.getPath();
        this.shouldRepair = false;
        init(tokens, map);
    }

    /**
     * Constructor
     * 
     * @param mxmlText input build our {@link MXMLData} from
     */
    public MXMLData(int startOffset, Reader mxmlText, IFileSpecification fileSpec)
    {
        TokenizerPayload payload = getTokens(startOffset, mxmlText);
        path = fileSpec.getPath();
        init(payload.getMXMLTokens(), payload.getPrefixMap());
    }

    /**
     * Constructor
     * 
     * @param mxmlText input build our {@link MXMLData} from
     */
    public MXMLData(int startOffset, Reader mxmlText, String path, boolean fullContent)
    {
        this.fullContent = fullContent;
        TokenizerPayload payload = getTokens(startOffset, mxmlText);
        this.path = path;
        init(payload.getMXMLTokens(), payload.getPrefixMap());
    }

    public MXMLData(Reader mxmlText, IFileSpecification fileSpec)
    {
        this(0, mxmlText, fileSpec);
    }

    public MXMLData(Reader mxmlText, String path)
    {
        // Some clients of MXML data pass in a null IPath for the source path.
        // Specifically this seems to happen in the code that builds a new
        // mxml document from a template.
        this(0, mxmlText, path != null ? path : null, true);
    }

    /**
     * Constructor
     * 
     * @param specification input build our {@link MXMLData} from
     */
    public MXMLData(IFileSpecification specification)
    {
        TokenizerPayload payload;
        try
        {
            path = specification.getPath();
            payload = getTokens(specification.createReader());
            init(payload.getMXMLTokens(), payload.getPrefixMap());
        }
        catch (FileNotFoundException e)
        {
            //do nothing
        }
    }

    private void init(List tokens, PrefixMap map)
    {
        mxmlDialect = MXMLDialect.getMXMLDialect(map);
        initializeFromTokens(tokens);
    }

    /**
     * Update the version of the mxml data
     * 
     * @param map the prefix map
     */
    public void updateMXMLVersion(PrefixMap map)
    {
        mxmlDialect = MXMLDialect.getMXMLDialect(map);
    }

    /**
     * Get the namespaces defined on the root tag
     * 
     * @param reader The reader which would provide the root tag information
     * @return {@link PrefixMap} for the root tag
     * @throws IOException error
     */
    public static PrefixMap getRootNamespaces(Reader reader) throws IOException
    {
        MXMLTokenizer tokenizer = new MXMLTokenizer();
        try
        {
            tokenizer.setReader(reader);
            return tokenizer.getRootTagPrefixMap().clone();
        }
        finally
        {
            tokenizer.close();
        }
    }

    protected TokenizerPayload getTokens(Reader reader)
    {
        return getTokens(0, reader);
    }

    protected TokenizerPayload getTokens(int startOffset, Reader reader)
    {
        List tokens = null;
        MXMLTokenizer tokenizer = new MXMLTokenizer(startOffset);
        try
        {
            tokenizer.setIsRepairing(true);
            tokens = tokenizer.parseTokens(reader);
            wasRepaired = tokenizer.tokensWereRepaired();
            return new TokenizerPayload(tokens, tokenizer.getPrefixMap());
        }
        finally
        {
            IOUtils.closeQuietly(tokenizer);
        }
    }

    @Override
    public MXMLDialect getMXMLDialect()
    {
        return mxmlDialect;
    }

    @Override
    public PrefixMap getPrefixMapForData(IMXMLTagData data)
    {
        PrefixMap result = nsMap.get(data);
        if (result != null)
            return result;
        if (data.isCloseTag())
        {
            IMXMLTagData openTagData = data.getParent().findTagOrSurroundingTagContainingOffset(data.getAbsoluteStart());
            if (openTagData != null)
                return nsMap.get(openTagData);
        }
        return null;
    }

    void removePrefixMappingForTag(IMXMLTagData data)
    {
        nsMap.remove(data);
    }

    void clearPrefixMappings()
    {
        nsMap.clear();
    }

    public void setPrefixMappings(HashMap map)
    {
        if (nsMap != null)
            nsMap.clear();
        nsMap = map;
    }

    public Map getTagToPrefixMap()
    {
        return nsMap;
    }

    /**
     * Returns a collection of prefix->namespaces mappings found within this
     * document. This map DOES NOT maintain order, and for more fine-grained
     * information, the getPrefixMap call on individual {@link IMXMLTagData} and
     * {@link IMXMLTagAttributeData} objects should be called
     * 
     * @return a prefix map
     */
    public PrefixMap getDocumentPrefixMap()
    {
        MutablePrefixMap map = new MutablePrefixMap();
        for (PrefixMap tagMap : nsMap.values())
        {
            assert tagMap != null;
            map.addAll(tagMap);
        }
        return map.toImmutable();
    }

    /**
     * Returns the PrefixMap for the root tag of this {@link MXMLData} object
     * 
     * @return a {@link PrefixMap} or null
     */
    public PrefixMap getRootTagPrefixMap()
    {
        IMXMLTagData rootTag = getRootTag();
        if (rootTag != null)
        {
            return nsMap.get(rootTag);
        }
        return null;
    }

    @Override
    public Collection getProblems()
    {
        return problems;
    }

    public boolean hasProblems()
    {
        return problems.size() > 0;
    }

    /**
     * Determines if this data was built from a source that was repaired
     * 
     * @return true if the underlying source was repaired
     */
    public boolean isDataRepaired()
    {
        return wasRepaired;
    }

    public void setWasRepaired(boolean wasRepaired)
    {
        this.wasRepaired = wasRepaired;
    }

    @Override
    public IFileSpecification getFileSpecification()
    {
        return new FileSpecification(path);
    }

    @Override
    public String getPath()
    {
        return path;
    }

    /**
     * API to change the path after MXMLData creation. An MXMLDocument makes an
     * MXMLData before the actual location is known.
     * 
     * @param path is the absolute path to the backing source file
     */
    public void setPath(String path)
    {
        this.path = path;
    }

    /**
     * Common code from the two constructors
     * 
     * @param tokens A list of MXML tokens.
     */
    protected void initializeFromTokens(List tokens)
    {
        cursor = new Cursor();
        parse(this, tokens, mxmlDialect, problems);
        cursor.reset();
    }

    /**
     * Use the MXML tokens to build MXMLUnitData.
     * 
     * @param data the {@link MXMLData} object
     * @param tokens the list of tokens to build this data from
     * @param dialect the {@link MXMLDialect} we are working against
     * @param incremental true if this data is being built incrementally. All
     * location updates will need to be done outside this element
     */
    private void parse(MXMLData data, List tokens, MXMLDialect dialect, Collection problems)
    {
        ArrayList units = new ArrayList(tokens.size() / 6);
        nsMap = new HashMap();
        MXMLUnitData unit = null;
        MXMLToken currentComment = null;
        FastStack depth = new FastStack(tokens.size() / 8);
        IFileSpecification spec = new FileSpecification(data.getPath() != null ? data.getPath() : "");
        depth.setStackDecorator(new IFastStackDecorator() {
            @Override
            public Integer decorate(Integer e)
            {
                if (e == null)
                    return -1;
                return e;
            }

        });
        int index = -1;
        int balancingIndex = 0;
        depth.push(index);
        ListIterator tokenIterator = tokens.listIterator();
        BalancingMXMLProcessor processor = new BalancingMXMLProcessor(getFileSpecification(), problems);
        while (tokenIterator.hasNext())
        {
            MXMLToken token = tokenIterator.next();
            switch (token.getType())
            {
                case MXMLTokenTypes.TOKEN_ASDOC_COMMENT:
                {
                    currentComment = token;
                    //treat this like text.
                    unit = new MXMLTextData(token);
                    units.add(unit);
                    index++;
                    if (fullContent)
                    {
                        unit.setParentUnitDataIndex(depth.peek());
                        unit.setLocation(data, index);
                    }
                    break;
                }
                
                case MXMLTokenTypes.TOKEN_COMMENT:
                case MXMLTokenTypes.TOKEN_CDATA:
                case MXMLTokenTypes.TOKEN_WHITESPACE:
                {
                    //treat this like text.
                    unit = new MXMLTextData(token);
                    units.add(unit);
                    index++;
                    if (fullContent)
                    {
                        unit.setParentUnitDataIndex(depth.peek());
                        unit.setLocation(data, index);
                    }
                    break;
                }
                
                case MXMLTokenTypes.TOKEN_OPEN_TAG_START:
                {
                    unit = new MXMLTagData();
                    MutablePrefixMap map = ((MXMLTagData)unit).init(this, token, tokenIterator, dialect, spec, problems);
                    ((MXMLTagData)unit).setCommentToken(currentComment);
                    currentComment = null;
                    units.add(unit);
                    index++;
                    if (fullContent)
                    {
                        unit.setParentUnitDataIndex(depth.peek());
                        unit.setLocation(data, index);
                        if (!((MXMLTagData)unit).isEmptyTag())
                            processor.addOpenTag((MXMLTagData)unit, balancingIndex);
                    }
                    if (map != null)
                        nsMap.put((MXMLTagData)unit, map.toImmutable());
                    if (!((MXMLTagData)unit).isEmptyTag())
                    {
                        depth.push(index);
                        balancingIndex++;
                    }
                    break;
                }
                
                case MXMLTokenTypes.TOKEN_CLOSE_TAG_START:
                {
                    unit = new MXMLTagData();
                    ((MXMLTagData)unit).init(this, token, tokenIterator, dialect, spec, problems);
                    ((MXMLTagData)unit).setCommentToken(currentComment);
                    if (!((MXMLTagData)unit).isEmptyTag())
                    {
                        depth.pop();
                        balancingIndex--;
                    }
                    index++;
                    if (fullContent)
                    {
                        unit.setLocation(data, index);
                        unit.setParentUnitDataIndex(depth.peek());
                        processor.addCloseTag((MXMLTagData)unit, balancingIndex);
                    }
                    currentComment = null;
                    units.add(unit);
                    break;
                }
                
                case MXMLTokenTypes.TOKEN_TEXT:
                {
                    unit = new MXMLTextData(token);
                    units.add(unit);
                    index++;
                    if (fullContent)
                    {
                        unit.setParentUnitDataIndex(depth.peek());
                        unit.setLocation(data, index);
                    }
                    break;
                }
                
                case MXMLTokenTypes.TOKEN_PROCESSING_INSTRUCTION:
                {
                    unit = new MXMLInstructionData(token);
                    units.add(unit);
                    index++;
                    if (fullContent)
                    {
                        unit.setParentUnitDataIndex(depth.peek());
                        unit.setLocation(data, index);
                    }
                    break;
                }
                
                default:
                {
                    problems.add(new SyntaxProblem(token));
                    break;
                }
            }
        }
        this.units = units.toArray(new IMXMLUnitData[0]);
        if (fullContent && shouldRepair)
        {
            this.units = processor.balance(this.units, this, nsMap);
            if (processor.wasRepaired())
            { //repaired, so let's rebuild our prefix maps and tag depths
                refreshPositionData();
            }
        }
    }

    /**
     * Used to rebuild the structures inside of the MXMLData, refreshing prefix
     * maps, depth and position data. Should only be used after calls that
     * rebuild the internal structure are called
     */
    public void refreshPositionData()
    {
        FastStack depth = new FastStack(units.length / 2);
        depth.setStackDecorator(new IFastStackDecorator() {
            @Override
            public Integer decorate(Integer e)
            {
                if (e == null)
                    return -1;
                return e;
            }

        });
        depth.clear();
        depth.push(-1);
        for (int i = 0; i < units.length; i++)
        {
            if (units[i] instanceof IMXMLTagData)
            {
                IMXMLTagData currentTag = (IMXMLTagData)units[i];
                if (currentTag.isCloseTag())
                {
                    if (!currentTag.isEmptyTag())
                        depth.pop();
                }
                ((MXMLTagData)currentTag).setParentUnitDataIndex(depth.peek());
                ((MXMLTagData)currentTag).setLocation(this, i);
                if (currentTag.isOpenTag())
                {
                    if (!currentTag.isEmptyTag())
                    {
                        depth.push(i);
                    }
                }
            }
            else
            {
                ((MXMLUnitData)units[i]).setParentUnitDataIndex(depth.peek());
                ((MXMLUnitData)units[i]).setLocation(this, i);
            }
        }
    }

    @Override
    public IMXMLUnitData[] getUnits()
    {
        return units;
    }

    public Iterator getUnitIterator()
    {
        return new MXMLUnitDataIterator(units);
    }

    /**
     * Replace the list of units in this MXMLData.
     * 
     * @param units units to add
     */
    public void setUnits(IMXMLUnitData[] units)
    {
        this.units = units;
    }

    @Override
    public IMXMLUnitData getUnit(int i)
    {
        if (i < 0 || i >= units.length)
            return null;
        return units[i];
    }

    @Override
    public int getNumUnits()
    {
        return units.length;
    }

    /**
     * If the offset is contained within an MXML unit, get that unit. If it's
     * not, then get the first unit that follows the offset.
     * 
     * @param offset test offset
     * @return unit that contains (or immediately follows) the offset A few
     * subtleties: In Royale we have endeavored to preserve the existing
     * definition of "nearest", so that for a given MXML file, royale will find
     * the same "nearest" unit.
     */

    protected IMXMLUnitData findNearestUnit(int offset)
    {

        // Use the cursor as a fast search hint. But only if the cursor is at or before the
        // are of interest.
        // TODO: do we care that this is not thread safe?
        int startOffset = 0;
        if (cursor.getOffset() <= offset)
            startOffset = cursor.getUnitIndex();

        // Sanity check
        if (startOffset < 0 || startOffset >= units.length)
            startOffset = 0;

        // Now iterate though the units and find the first one that is acceptable
        IMXMLUnitData ret = null;
        for (int i = startOffset; (i < units.length) && (ret == null); i++)
        {
            IMXMLUnitData unit = units[i];

            // unit is a match if it "contains" the offset.
            // We are using a somewhat bizarre form of "contains" here, in that we are
            // using getStart() and getConentEnd(). This asymmetric mismatch is for several reasons:
            //      * it's the only way to match the existing (non-royale) behavior
            //      * If our cursor is before the <, we want to match the tag.
            //              example:     |  will find "foo" as the nearest tag.
            //      So we need to use start here (not content start)
            //      * If our cursor is between two tags, we want to match the NEXT one, not the previous one
            //              example:   |  should match foo, not bar

            if (MXMLData.contains(unit.getAbsoluteStart(), unit.getContentEnd(), offset))
            {
                ret = unit;
            }
            // if we find a unit that starts after the offset, then it must
            // be the "first one after", so return it
            else if (unit.getAbsoluteStart() >= offset)
            {
                ret = unit;
            }
        }

        // If we found something, update the cursor for the next search
        if (ret != null)
            cursor.setCursor(offset, ret.getIndex());
        return ret;
    }

    /**
     * Get the unit that should be referenced when looking at what tags contain
     * this offset (i.e. tags that are opened and not closed before this offset)
     * 
     * @param offset test offset
     * @return reference unit for containment searches
     */
    public IMXMLUnitData findContainmentReferenceUnit(int offset)
    {
        return findNearestUnit(offset);
    }

    /**
     * Get the unit that contains this offset
     * 
     * @param offset test offset
     * @return the containing unit (or null if no unit contains this offset)
     */
    public IMXMLUnitData findUnitContainingOffset(int offset)
    {
        IMXMLUnitData unit = findNearestUnit(offset);
        if (unit != null && unit.containsOffset(offset))
            return unit;

        return null;
    }

    /**
     * Get the open, close, or empty tag that contains this offset. Note that if
     * offset is inside a text node, this returns null. If you want the
     * surrounding tag in that case, use
     * findTagOrSurroundingTagContainingOffset.
     * 
     * @param offset test offset
     * @return the containing tag (or null, if no tag contains this offset)
     */
    public IMXMLTagData findTagContainingOffset(int offset)
    {
        IMXMLUnitData unit = findUnitContainingOffset(offset);
        if (unit != null && unit.isTag())
            return (IMXMLTagData)unit;

        return null;
    }

    @Override
    public IMXMLTagData findTagOrSurroundingTagContainingOffset(int offset)
    {
        IMXMLUnitData unit = findUnitContainingOffset(offset);
        if (unit != null)
        {
            if (unit.isTag())
            {
                return (IMXMLTagData)unit;
            }
            else if (unit.isText())
            {
                IMXMLTagData containingTag = unit.getContainingTag(unit.getAbsoluteStart());
                return containingTag;
            }
        }

        return null;
    }

    /**
     * Get the open or empty tag whose attribute list contains this offset. A
     * tag's attribute list extends from after the tag name + first whitespace
     * until before the closing ">" or "/>".
     * 
     * @param offset test offset
     * @return tag whose attribute list contains this offset (or null, if the
     * offset isn't in any attribute lists
     */
    public IMXMLTagData findAttributeListContainingOffset(int offset)
    {
        IMXMLTagData tag = findTagContainingOffset(offset);
        if (tag != null && tag.isOpenTag())
            return tag.isOffsetInAttributeList(offset) ? tag : null;

        return null;
    }

    /**
     * Test whether the offset is contained within the range from start to end.
     * This test excludes the start position and includes the end position,
     * which is how you want things to work for code hints.
     * 
     * @param start start of the range (excluded)
     * @param end end of the range (included)
     * @param offset test offset
     * @return true iff the offset is contained in the range
     */
    public static boolean contains(int start, int end, int offset)
    {
        return start < offset && end >= offset;
    }

    @Override
    public IMXMLTagData getRootTag()
    {
        int n = units.length;
        for (int i = 0; i < n; i++)
        {
            IMXMLUnitData unit = units[i];
            if (unit instanceof IMXMLTagData)
                return (IMXMLTagData)unit;
        }
        return null;
    }

    @Override
    public int getEnd()
    {
        final int n = getNumUnits();
        return n > 0 ? getUnit(n - 1).getAbsoluteEnd() : 0;
    }

    public Cursor getCursor()
    {
        return cursor;
    }

    /**
     * Verifies that all units (plus all attributes on units that are tags) have
     * their source location information set.
     * 

* This is used only in asserts. */ public boolean verify() { for (IMXMLUnitData unit : getUnits()) { ((MXMLUnitData)unit).verify(); } return true; } // For debugging only. void dumpUnits() { for (IMXMLUnitData unit : getUnits()) { System.out.println(((MXMLUnitData)unit).toDumpString()); } } /** * For debugging only. */ @Override public String toString() { StringBuffer sb = new StringBuffer(); IMXMLUnitData[] units = getUnits(); int n = units.length; for (int i = 0; i < n; i++) { // Display the unit's index as, for example, [3]. sb.append('['); sb.append(i); sb.append(']'); sb.append(' '); // Display the unit's information. sb.append(units[i].toString()); sb.append('\n'); } return sb.toString(); } public static void main(String[] args) { final FileSpecification fileSpec = new FileSpecification(args[0]); final MXMLTokenizer tokenizer = new MXMLTokenizer(fileSpec); List tokens = null; try { tokens = tokenizer.parseTokens(fileSpec.createReader()); // Build tags and attributes from the tokens. final MXMLData mxmlData = new MXMLData(tokens, tokenizer.getPrefixMap(), fileSpec); mxmlData.dumpUnits(); } catch (FileNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { IOUtils.closeQuietly(tokenizer); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy