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

org.apache.myfaces.custom.inputHtml.InputHtml Maven / Gradle / Ivy

/*
 * 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.myfaces.custom.inputHtml;

import javax.faces.FacesException;
import javax.faces.application.Resource;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIViewRoot;
import javax.faces.component.UniqueIdVendor;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.event.ComponentSystemEvent;
import javax.faces.event.ListenerFor;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.myfaces.buildtools.maven2.plugin.builder.annotation.JSFComponent;
import org.apache.myfaces.component.html.ext.HtmlInputText;
import org.apache.myfaces.shared_tomahawk.renderkit.RendererUtils;
import org.apache.myfaces.shared_tomahawk.renderkit.html.HtmlRendererUtils;
import org.apache.myfaces.shared_tomahawk.renderkit.html.util.FormInfo;
import org.apache.myfaces.shared_tomahawk.renderkit.html.util.JavascriptUtils;
import org.apache.myfaces.tomahawk.application.PreRenderViewAddResourceEvent;
import org.apache.myfaces.tomahawk.util.TomahawkResourceUtils;

/**
 * HTML Editor using the kupu library.
 * http://kupu.oscom.org/
 *
 * An inline HTML based word processor based on the Kupu library. 
 * 
 * See http://kupu.oscom.org 
 * 
 * Right now, the support is limited to one editor per page 
 * (but you can use tabs to have multiple editors, but only 
 * one rendered at a time). 
 * 
 * Unless otherwise specified, all attributes accept static values or EL expressions.
 *
 * @author Sylvain Vieujot (latest modification by $Author: skitching $)
 * @version $Revision: 673833 $ $Date: 2008-07-03 16:58:05 -0500 (jue, 03 jul 2008) $
 */
@JSFComponent(
   name = "t:inputHtml",
   tagClass = "org.apache.myfaces.custom.inputHtml.InputHtmlTag",
   composite=true)
@ListenerFor(systemEventClass=PreRenderViewAddResourceEvent.class)
public class InputHtml extends HtmlInputText implements NamingContainer, UniqueIdVendor {
    public static final String COMPONENT_TYPE = "org.apache.myfaces.InputHtml";

    public static final String DEFAULT_RENDERER_TYPE = "org.apache.myfaces.InputHtml";

    private static final String INPUT_HTML_LIBRARY = "oam.custom.inputHtml";
    private static final String INPUT_HTML_LIBRARY_KUPU_DRAWERS = "oam.custom.inputHtml.kupudrawers";

    private static final Log log = LogFactory.getLog(HtmlInputText.class);

    public InputHtml() {
        setRendererType(DEFAULT_RENDERER_TYPE);
    }
    
    @Override
    public void setRendererType(String rendererType)
    {
        if (!"javax.faces.Composite".equals(rendererType))
        {
            super.setRendererType(rendererType);
        }
    }
    
    public void processEvent(ComponentSystemEvent event)
    {
        super.processEvent(event);
        if (event instanceof PreRenderViewAddResourceEvent)
        {
            InputHtml editor = (InputHtml) event.getComponent();
            if( !HtmlRendererUtils.isDisplayValueOnly(editor) && !useFallback(editor))
            {
                FacesContext facesContext = FacesContext.getCurrentInstance();
                //AddResource addResource = AddResourceFactory.getInstance(facesContext);
                //addResource.addStyleSheet(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupustyles.css");
                TomahawkResourceUtils.addOutputStylesheetResource(facesContext, INPUT_HTML_LIBRARY, "myFacesKupustyles.css");
                //addResource.addStyleSheet(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupudrawerstyles.css");
                TomahawkResourceUtils.addOutputStylesheetResource(facesContext, INPUT_HTML_LIBRARY, "kupudrawerstyles.css");
                //addResource.addStyleSheet(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "myFaces.css");
                TomahawkResourceUtils.addOutputStylesheetResource(facesContext, INPUT_HTML_LIBRARY, "myFaces.css");
    
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "sarissa.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "sarissa.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "sarissa_ieemu_xpath.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "sarissa_ieemu_xpath.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupuhelpers.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupuhelpers.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupueditor.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupueditor.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupubasetools.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupubasetools.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupuloggers.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupuloggers.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupunoi18n.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupunoi18n.js");
                //addResource.addJavaScriptAtPosition(context, InputHtmlRenderer.class, "i18n/i18n.js"); //NO
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupucleanupexpressions.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupucleanupexpressions.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupucontentfilters.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupucontentfilters.js");
    
                if (editor.isShowAnyToolBox())
                {
                    //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kuputoolcollapser.js");
                    TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kuputoolcollapser.js");
                }
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupucontextmenu.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupucontextmenu.js");
    
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupuinit.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupuinit.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupustart.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupustart.js");
    
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupusourceedit.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupusourceedit.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupuspellchecker.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupuspellchecker.js");
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupudrawers.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupudrawers.js");
    
                //addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "kupuundo.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "kupuundo.js");
                //addResource.addJavaScriptAtPosition(context, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "diff_match_patch.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "diff_match_patch.js");
    
                //addResource.addJavaScriptAtPosition(facesContext, AddResource.HEADER_BEGIN, InputHtmlRenderer.class, "myFacesUtils.js");
                TomahawkResourceUtils.addOutputScriptResource(facesContext, INPUT_HTML_LIBRARY, "myFacesUtils.js");
                
                if( editor.getStyle()!=null ){
                    // Convert the style into an style declaration so that it doesn't preempt the Zoom works as it's relying on changing the class
                    //addResource.addInlineStyleAtPosition(
                    //        context, AddResource.HEADER_BEGIN,
                    //        "#kupu-editor{height: inherit;}\n"+
                    //        "div.kupu-fulleditor{"+editor.getStyle()+"}");
                    TomahawkResourceUtils.addInlineOutputStylesheetResource(facesContext, 
                            "#kupu-editor{height: inherit;}\n"+
                            "div.kupu-fulleditor{"+editor.getStyle()+"}");
                }
            }
        }
    }
    
    private static boolean useFallback(InputHtml editor){
        // TODO : Handle fallback="auto"
        return editor.getFallback().equals("true");
    }
    /*
    
      
        
  #{resource['oam.custom.inputHtml:fulldoc.html']}
  1
  0
  1
  application/xhtml+xml
  1
  
    plain
    listing
    grid
    data
  

  
    
      Convert single quotes to curly ones
      
        
          (\W)'
        
        
          \1‘
        
      
      
        
          '
        
        
      
    
    
      Reduce whitespace
      
        
          [\n\r\t]
        
        
          \x20
        
      
      
        
          [ ]{2}
        
        
          \x20
        
      
    
  

  #{resource['oam.custom.inputHtml.kupudrawers:drawer.xsl']}
  #{resource['oam.custom.inputHtml.kupudrawers:drawer.xsl']}
  #{resource['oam.custom.inputHtml.kupudrawers:imagelibrary.xml']}
  #{resource['oam.custom.inputHtml.kupudrawers:linklibrary.xml']}
   
   

      
    
   */
    public String getKupuConfig()
    {
        StringBuilder builder = new StringBuilder();
        builder.append("");
        builder.append("");
        builder.append("fulldoc.html");
        builder.append("1");
        builder.append("0");
        builder.append("1");
        builder.append("application/xhtml+xml");
        builder.append("1");
        builder.append("");
        builder.append("plain");
        builder.append("listing");
        builder.append("grid");
        builder.append("data");
        builder.append("");

        builder.append("");
        builder.append("");
        builder.append("Convert single quotes to curly ones");
        builder.append("");
        builder.append("");
        builder.append("(\\W)'");
        builder.append("");
        builder.append("");
        builder.append("\1‘");
        builder.append("");
        builder.append("");
        builder.append("");
        builder.append("");
        builder.append("'");
        builder.append("");
        builder.append("");
        builder.append("’");
        builder.append("");
        builder.append("");
        builder.append("");
        builder.append("");
        builder.append("Reduce whitespace");
        builder.append("");
        builder.append("");
        builder.append("[\n\r\t]");
        builder.append("");
        builder.append("");
        builder.append("\\x20");
        builder.append("");
        builder.append("");
        builder.append("");
        builder.append("");
        builder.append("[ ]{2}");
        builder.append("");
        builder.append("");
        builder.append("\\x20");
        builder.append("");
        builder.append("");
        builder.append("");
        builder.append("");

        FacesContext context = getFacesContext();
        Resource resource = context.getApplication().getResourceHandler()
                .createResource("drawer.xsl",
                        INPUT_HTML_LIBRARY_KUPU_DRAWERS);
        builder.append("" + resource.getRequestPath()
                + "");
        builder.append("" + resource.getRequestPath()
                + "");

        resource = context.getApplication().getResourceHandler()
                .createResource("imagelibrary.xml",
                        INPUT_HTML_LIBRARY_KUPU_DRAWERS);
        builder.append("" + resource.getRequestPath()
                + "");

        resource = context.getApplication().getResourceHandler()
                .createResource("linklibrary.xml",
                        INPUT_HTML_LIBRARY_KUPU_DRAWERS);
        builder.append("" + resource.getRequestPath()
                + "");

        builder.append(" ");
        builder.append(" ");

        builder.append("");
        builder.append("");
        return builder.toString();
    }
    
    
    public String getDisplayValueOnlyText()
    {
        return getHtmlBody(RendererUtils.getStringValue(getFacesContext(), this));
    }
    
    public String getFallbackText()
    {
        String text = RendererUtils.getStringValue(getFacesContext(), this);
        return htmlToPlainText(text, this);
    }
    
    public String getHiddenText()
    {
        return RendererUtils.getStringValue(getFacesContext(), this);
    }
    
    private static String htmlToPlainText(String html, InputHtml editor){
        return editor.getHtmlBody( html )
                .replaceAll("","\n")
                .replaceAll("<.+?>", "");
    }
    
    
    public String getFormId()
    {
        FormInfo parentFormInfo = RendererUtils.findNestingForm(this, getFacesContext());
        if(parentFormInfo == null)
            throw new FacesException("InputHtml must be embedded in a form.");
        return parentFormInfo.getFormName(); 
    }
    
    public String getEncodedText()
    {
        String text = this.getValueAsHtmlDocument(getFacesContext());
        return (text == null) ? "" : JavascriptUtils.encodeString( text );

    }
    
    /**
     * 
     * {@inheritDoc}
     * 
     * @since 2.0
     */
    public String createUniqueId(FacesContext context, String seed)
    {
        StringBuilder bld = new StringBuilder();

        Long uniqueIdCounter = (Long) getStateHelper().get(PropertyKeys.uniqueIdCounter);
        uniqueIdCounter = (uniqueIdCounter == null) ? 0 : uniqueIdCounter;
        getStateHelper().put(PropertyKeys.uniqueIdCounter, (uniqueIdCounter+1L));
        // Generate an identifier for a component. The identifier will be prefixed with UNIQUE_ID_PREFIX, and will be unique within this UIViewRoot. 
        if(seed==null)
        {
            return bld.append(UIViewRoot.UNIQUE_ID_PREFIX).append(uniqueIdCounter).toString();    
        }
        // Optionally, a unique seed value can be supplied by component creators which should be included in the generated unique id.
        else
        {
            return bld.append(UIViewRoot.UNIQUE_ID_PREFIX).append(seed).toString();
        }
    }    
    
    /**
     * Use a text area instead of the javascript HTML editor. 
     * 
     * Default is false. Use with caution.
     * 
     * @JSFProperty
     */
    public String getFallback(){
        return (String) getStateHelper().eval(PropertyKeys.fallback, "false");
    }
    public void setFallback(String _fallback){
        getStateHelper().put(PropertyKeys.fallback, _fallback);
    }

    /**
     * The type of the value. It can be either fragment for an HTML 
     * fragment (default) or document for a full HTML document, with 
     * head, title, body, ... tags.
     * 
     * @JSFProperty
     */
    public String getType(){
        return (String) getStateHelper().eval(PropertyKeys.type, "fragment");
    }
    public void setType(String _type){
        getStateHelper().put(PropertyKeys.type, _type);
    }
    public boolean isTypeDocument(){
        return getType().equals("document");
    }

    /**
     * Allows the user to edit the HTML source code. Default is true.
     * 
     * @JSFProperty
     */
    public boolean isAllowEditSource(){
        return (Boolean) getStateHelper().eval(PropertyKeys.allowEditSource, Boolean.TRUE);
    }
    public void setAllowEditSource(boolean allowEditSource){
        getStateHelper().put(PropertyKeys.allowEditSource, allowEditSource);
    }

    /**
     * Allows the user to insert external links. Default is true.
     * 
     * @JSFProperty
     */
    public boolean isAllowExternalLinks(){
        return (Boolean) getStateHelper().eval(PropertyKeys.allowExternalLinks, Boolean.TRUE);
    }
    public void setAllowExternalLinks(boolean allowExternalLinks){
        getStateHelper().put(PropertyKeys.allowExternalLinks, allowExternalLinks);
    }

    /**
     * Show the Kupu Logo in the buttons bar. Default is true.
     * 
     * @JSFProperty
     */
    public boolean isAddKupuLogo(){
        return (Boolean) getStateHelper().eval(PropertyKeys.addKupuLogo, Boolean.TRUE);
    }
    public void setAddKupuLogo(boolean addKupuLogo){
        getStateHelper().put(PropertyKeys.addKupuLogo, addKupuLogo);
    }

    /**
     * Shortcut to avoid setting all the showXXToolBox to true. Default is false.
     * 
     * @JSFProperty
     */
    public boolean isShowAllToolBoxes(){
        return (Boolean) getStateHelper().eval(PropertyKeys.showAllToolBoxes, Boolean.FALSE);
    }
    public void setShowAllToolBoxes(boolean showAllToolBoxes){
        getStateHelper().put(PropertyKeys.showAllToolBoxes, showAllToolBoxes);
    }

    /**
     * Show the Properties tool box next to the text. Default is false.
     * 
     * @JSFProperty
     */
    public boolean isShowPropertiesToolBox(){
        if( isShowAllToolBoxes() )
            return true;

        return (Boolean) getStateHelper().eval(PropertyKeys.showPropertiesToolBox, Boolean.FALSE);
    }

    public void setShowPropertiesToolBox(boolean showPropertiesToolBox){
        getStateHelper().put(PropertyKeys.showPropertiesToolBox, showPropertiesToolBox);
    }

    /**
     * Show the Links tool box next to the text. Default is false.
     * 
     * @JSFProperty
     */
    public boolean isShowLinksToolBox(){
        if( isShowAllToolBoxes() )
            return true;

        return (Boolean) getStateHelper().eval(PropertyKeys.showLinksToolBox, Boolean.FALSE);
    }
    
    public void setShowLinksToolBox(boolean showLinksToolBox){
        getStateHelper().put(PropertyKeys.showLinksToolBox, showLinksToolBox);
    }

    /**
     * Show the Images tool box next to the text. Default is false.
     * 
     * @JSFProperty
     */
    public boolean isShowImagesToolBox(){
        if( isShowAllToolBoxes() )
            return true;

        return (Boolean) getStateHelper().eval(PropertyKeys.showImagesToolBox, Boolean.FALSE);
    }
    public void setShowImagesToolBox(boolean showImagesToolBox){
        getStateHelper().put(PropertyKeys.showImagesToolBox, showImagesToolBox);
    }

    /**
     * Show the Tables tool box next to the text. Default is false.
     * 
     * @JSFProperty
     */
    public boolean isShowTablesToolBox(){
        if( isShowAllToolBoxes() )
            return true;

        return (Boolean) getStateHelper().eval(PropertyKeys.showTablesToolBox, Boolean.FALSE);
    }
    public void setShowTablesToolBox(boolean showTablesToolBox){
        getStateHelper().put(PropertyKeys.showTablesToolBox, showTablesToolBox);
    }

    /**
     * Show the Cleanup Expressions tool box next to the text. Default is false.
     * 
     * @JSFProperty
     */
    public boolean isShowCleanupExpressionsToolBox(){
        if( isShowAllToolBoxes() )
            return true;

        return (Boolean) getStateHelper().eval(PropertyKeys.showCleanupExpressionsToolBox, Boolean.FALSE);
    }
    public void setShowCleanupExpressionsToolBox(boolean showCleanupExpressionsToolBox){
        getStateHelper().put(PropertyKeys.showCleanupExpressionsToolBox, showCleanupExpressionsToolBox);
    }

    /**
     * Show the Debug tool box next to the text. Default is false.
     * 
     * @JSFProperty
     */
    public boolean isShowDebugToolBox(){
        if( isShowAllToolBoxes() )
            return true;

        return (Boolean) getStateHelper().eval(PropertyKeys.showDebugToolBox, Boolean.FALSE);
    }
    public void setShowDebugToolBox(boolean showTablesToolBox){        
        getStateHelper().put(PropertyKeys.showDebugToolBox, showTablesToolBox);
    }

    public boolean isShowAnyToolBox(){
           return isShowAllToolBoxes()
               || isShowPropertiesToolBox()
               || isShowLinksToolBox()
               || isShowImagesToolBox()
               || isShowTablesToolBox()
               || isShowCleanupExpressionsToolBox()
               || isShowDebugToolBox();
    }

    public String getValueAsHtmlDocument(FacesContext context){
        String val = RendererUtils.getStringValue(context, this);
        if( isHtmlDocument( val ) )
            return val;

        return ""+(val==null ? "" : val)+"";
    }

    private static boolean isHtmlDocument(String text){
        if( text == null )
            return false;

        if( text.indexOf("")!=-1 || text.indexOf("")!=-1 || text.indexOf("") )
            return fragment.substring(0, fragment.length()-6);
        return fragment;
    }
    
    String getHtmlBody(String html){
        html = html.trim();
        if( html.length() == 0 )
            return "";

        String lcText = html.toLowerCase();
        int textLength = lcText.length();
        int bodyStartIndex = -1;
        while(bodyStartIndex < textLength){
            bodyStartIndex++;
            bodyStartIndex = lcText.indexOf("' )
                break;

            if( c!=' ' && c!='\t' )
                continue;

            bodyStartIndex = lcText.indexOf('>', bodyStartIndex);
            break;
        }
        bodyStartIndex++;

        int bodyEndIndex = lcText.lastIndexOf("")-1;
        
        if( bodyStartIndex<0 || bodyEndIndex<0
           || bodyStartIndex > bodyEndIndex
           || bodyStartIndex>=textLength || bodyEndIndex>=textLength ){

            if( lcText.indexOf("")!=-1 || lcText.indexOf("")!=-1 )
                return "";
            
            int htmlStartIndex = lcText.indexOf("");
            int htmlEndIndex = lcText.indexOf("");
            if( htmlStartIndex != -1 && htmlEndIndex > htmlStartIndex )
                return html.substring(htmlStartIndex+6, htmlEndIndex);
            
            if( isTypeDocument() )
                log.warn("Couldn't extract HTML body from :\n"+html);
            return html.trim();
        }

        return html.substring(bodyStartIndex, bodyEndIndex+1).trim();
    }
    
    protected enum PropertyKeys
    {
        fallback,
        type,
        allowEditSource,
        allowExternalLinks,
        addKupuLogo,
        showAllToolBoxes,
        showPropertiesToolBox,
        showLinksToolBox,
        showImagesToolBox,
        showTablesToolBox,
        showCleanupExpressionsToolBox,
        showDebugToolBox,
        uniqueIdCounter
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy