`
wansong
  • 浏览: 20657 次
  • 性别: Icon_minigender_1
  • 来自: 苏州
最近访客 更多访客>>
文章分类
社区版块
存档分类
最新评论

Apache之Digester

阅读更多
Digester是apache开源项目Commons中的一个子项目,是一款解析处理XML文档的工具。现在Java领域中流传了很多有关处理XML文档解析的工具,除官方(Sun)的标准的SAX(最新版本2.0),DOM(最新版本3.0,在Tiger版本中集成)外[JAXP只是Sun定义的一组规范接口],其他开源不泛多多,比如Jdom,Dom4j,Castor等等,包括这款Apache的digester。说到这里,你不得不佩服开源组织的强大智慧的结晶,digester处理XML文档基于XML节点树Path的规则,实在给人一种赏心悦目之感,这也是偶一直对其情有独钟的最大理由。废话免了,转入正题。
   刚说到Digester处理是基本类似于XML文档树节点遍历的规则来进行处理,底层处理是采用了SAX,基于事件驱动的模式。举个例子:
   <Company>
     <Technology>
       <name length="4">Corx</name>
       <date>2005.06.27</date>
     </Technology>
   
     <Product>
       <name length="4">Kxcp</name>
       <date>2004.12.29</date>
     </Product>
   </Company>
 
   在digester中,定义了一些规则(rule),对遍历的节点path预先对应好要处理的规则,即当解析器遍历到某个节点的时候,如果发现当前节点有对应的处理规则,调用相应的rule进行处理。举个例子:
   Company/Technology -> ObjectCreatedRule //对象创建规则
   Company/Technology/name -> BeanPropertySetterRule //属性存取规则
   ...
 
   对以上的解释可能还不太明白,不要着急,下面详细解释一下digester的基本原理,喝杯咖啡,慢慢来~
 
   首先看看org.apache.commons.digester.Digester这个类,查看source发现Digester本身继承了 DefaultHandler句柄,DefaultHandler句柄是SAX中基于时间驱动的缺省的句柄实现(包含ContentHandler, ErrorHandler, EntityResolver, DTDHandler),这个句柄不用多介绍了吧,相信用过SAX的哥们都明白。:)。刚刚不是说到了Rule了嘛,digester中定义了一个规则处理接口org.apache.commons.digester.Rule,此接口类似于ContentHandler接口中的方法,稍稍有点不同,主要有begin(), body(), end(), finish()方法。而digester缺省定义了许多有效的常用规则,每个规则都实现这个接口, 如果没有什么特殊需求,一般这些规则是够用了,罗列一下:BeanPropertySetterRule, CallMethodRule, CallParamRule, FactoryCreateRule, NodeCreateRule, ObjectCreateRule, ObjectParamRule,PathCallParamRule, SetNestedPropertiesRule, SetNextRule, SetPropertiesRule, SetPropertyRule, SetRootRule, SetTopRule,这些规则的意思稍后说。同时,对这些规则,digester还定义了一个规则的容器接口Rules(抽象类),这个抽象类接口容器容纳规则,并定义了规则匹配的模式,digester实现了一个基本的匹配模式RulesBase,简要看看这个实现中的两个最重要的方法:
   .....
   public void add(String pattern, Rule rule) {
         // to help users who accidently add '/' to the end of their patterns
         int patternLength = pattern.length();
         if (patternLength>1 && pattern.endsWith("/" ) {
             pattern = pattern.substring(0, patternLength-1);
         }
       
       
         List list = (List) cache.get(pattern);
         if (list == null) {
             list = new ArrayList();
             cache.put(pattern, list);
         }
         list.add(rule);
         rules.add(rule);
         if (this.digester != null) {
             rule.setDigester(this.digester);
         }
         if (this.namespaceURI != null) {
             rule.setNamespaceURI(this.namespaceURI);
         }

     }

还有一个方法:
   ...
   public List match(String namespaceURI, String pattern) {

         // List rulesList = (List) this.cache.get(pattern);
         List rulesList = lookup(namespaceURI, pattern);
         if ((rulesList == null) || (rulesList.size() < 1)) {
             // Find the longest key, ie more discriminant
             String longKey = "";
             Iterator keys = this.cache.keySet().iterator();
             while (keys.hasNext()) {
                 String key = (String) keys.next();
                 if (key.startsWith("*/" ) {
                     if (pattern.equals(key.substring(2)) ||
                         pattern.endsWith(key.substring(1))) {
                         if (key.length() > longKey.length()) {
                             // rulesList = (List) this.cache.get(key);
                             rulesList = lookup(namespaceURI, key);
                             longKey = key;
                         }
                     }
                 }
             }
         }
         if (rulesList == null) {
             rulesList = new ArrayList();
         }
         return (rulesList);
     }
     ...
   
     以上基本实现只是digester默认匹配规则,如果你要更换自己的规则匹配模式,则只需要继承 org.apache.commons.digester.Rules接口,定义自己的匹配方式,digester同时还给我们提供了一个比较复杂,不过非常常用的匹配模式,那就是通配符匹配模式,引入了”!“、”*“、”?“三个符号进行通配的匹配模式,这个类就是 org.apache.commons.digester.ExtendsBaseRules,后续再说。
   
     digester就是通过以上的几种接口组件,同时配合操作数栈,进行XML解析。具体说,就是在parse XML文档之前,预先向容器集合(默认就是RulesBase容器)对XML文档中的节点path注入匹配规则,然后在parse文档的时候,遭遇到节点时时,调用SAX句柄中相应的方法,配合操作数栈,根据定义好的匹配模式,调用相应规则中的方法,将XML序列化成Java Object。介绍有点抽象,沿用digester本身带的例子介绍一下:
     ...
     //对如下的XML文档
     <address-book>
       <person id="1" category="acquaintance">
         <name>Gonzo</name>
         <email type="business">gonzo@muppets.com</email>
       </person>
       <person id="2" category="rolemodel">
         <name>Kermit</name>
         <email type="business">kermit@muppets.com</email>
         <email type="home">kermie@acme.com</email>
       </person>
     </address-book>
     ...
...
   
     Digester digester = new Digester();
   
     AddressBook book = new AddressBook();
     d.push(book); //将AddressBook实例压入堆栈
   
     digester.addObjectCreate("address-book/person", Person.class);//对person节点注入对象创建规则,即在SAX的事件遭遇到person节点的时候,创建Person类的实例,并压入堆栈,此时堆栈中从栈顶到栈底分别为AddressBook实例,Person类实例。
     digester.addSetProperties("address-book/person" ;// 对person节点注入属性设置规则,即在SAX的事件遭遇到person节点中的Attributes时,根据属性列表中的属性值对,这儿就是 id="1", category="acquaintance",使用Java反射(reflection)机制,调用当前栈顶对象即Person实例类中id、 category属性的标准的JavaBean方法,setId, setCategory。
   
     digester.addSetNext("address-book/person", "addPerson" ;// 对person节点注入父节点方法调用规则,即在SAX事件遭遇到person节点的时候,调用栈中Person实例的父实例中的addPerson方法。d.addCallMethod("address-book/person/name", "setName", 0);//对name节点注入方法调用规则,调用当前栈顶对象即Person实例中的setName方法,而此方法的参数即是当前name节点的字符内容。通常这个规则和addCallParam规则配合使用,这儿是一种特殊情况。
     digester.addCallMethod("address-book/person/email", "addEmail", 2);//对email节点注入方法调用规则,调用当前栈顶对象即Person实例中的addEmail方法,此方法需要两个参数,一个是从属性值的 type属性获取,一个是从email本身的字符内容获取。
     digester.addCallParam("address-book/person/email", 0, "type" ;//对email节点注入参数调用规则,将当前节点的type属性值压入方法操作数栈
     digester.addCallParam("address-book/person/email", 1);//对email节点注入参数调用规则,将当前节点的字符属性值压入方法操作数栈。
   
     System.out.println(book);//打印book中值。。
     ...
   
     通过以上注释应该不难理解吧。
   
     下面再对以上所说的几种常用规则作一个详细的介绍:
     ObjectCreateRule:这个规则比较简单,此规则就是对指定的模式创建一个类的实例,并将当前实例压入堆栈,并且在遭遇元素结束
     时,将当前的栈顶实例弹出栈。
     对应Digester中有关这个规则的Javadoc方法说明:
       addObjectCreate(java.lang.String pattern, java.lang.Class clazz) - 方法参数说明了一切
       addObjectCreate(java.lang.String pattern, java.lang.String className) - 同上
       addObjectCreate(java.lang.String pattern, java.lang.String attributeName, java.lang.Class clazz) - 这个稍微解释一下,
       多了一个参数attributeName,这个参数的意思就是如果在当前匹配模式的节点中定义了属性,则默认就采用这个attributeName所
       对应的值来加载实例。比如以上面的例子加入person元素还有一个属性, class="test.org.apache.commons.digester.Person1",
       则此规则会加载Person1实例而不是Person实例。明白?!
       addObjectCreate(java.lang.String pattern, java.lang.String className, java.lang.String attributeName) -同上
     
     FactoryCreateRule:这个规则是基于工厂模式创建指定模式的一个类的实例。跟ObjectCreateRule类似,不同的是其参数Class继承了
     ObjectCreationFactory接口。此接口中有个方法createObject(Attributes attrs),创建类的实例,在规则中将此类的实例压入堆栈。
     对应Digester中有关这个规则的Javadoc方法说明:
       addFactoryCreate(java.lang.String pattern, java.lang.Class clazz) - 一目了然,不用说了。只是clazz必须是实现了
       ObjectCreationFactory接口的类。
       addFactoryCreate(java.lang.String pattern, java.lang.Class clazz, boolean ignoreCreateExceptions) - 同上,多了一个参
       数,ignoreCreateExceptions表明是否忽略在创建类的过程中忽略抛出的exception。
       addFactoryCreate(java.lang.String pattern, java.lang.Class clazz, java.lang.String attributeName) - 稍微介绍一下这里
       的attributeName参数,这个参数跟ObjectCreationRule规则中的attributeName雷同,不同的是这个属性的值必须是实现了接口
       ObjectCreationFactory接口的类。
       addFactoryCreate(java.lang.String pattern, java.lang.Class clazz, java.lang.String attributeName, boolean ignoreCreateExceptions)
       - 同上
       addFactoryCreate(java.lang.String pattern, ObjectCreationFactory creationFactory) - 同上
       addFactoryCreate(java.lang.String pattern, ObjectCreationFactory creationFactory, boolean ignoreCreateExceptions) - 同上
       addFactoryCreate(java.lang.String pattern, java.lang.String className) - 同上
       addFactoryCreate(java.lang.String pattern, java.lang.String className, boolean ignoreCreateExceptions) - 同上
       addFactoryCreate(java.lang.String pattern, java.lang.String className, java.lang.String attributeName) - 同上
       addFactoryCreate(java.lang.String pattern, java.lang.String className, java.lang.String attributeName, boolean ignoreCreateExceptions)

BeanPropertySetterRule:Bean属性设置规则,对匹配当前指定模式的元素设置bean属性同名或者指定属性的值。
     对应Digester中有关这个规则的Javadoc方法说明:
       addBeanPropertySetter(java.lang.String pattern)   - 对匹配当前指定模式的元素设置bean属性同名属性的值。
       addBeanPropertySetter(java.lang.String pattern, java.lang.String propertyName)   - 对匹配当前指定模式的元素设置bean属性指定属性的值。
     
     CallMethodRule:方法调用规则,对匹配当前指定模式的元素,初始化指定的方法类型和方法参数,并压入堆栈。此规则需要和CallMethodRule
     规则配合使用:
     对应Digester中有关这个规则的Javadoc方法说明:
       addCallMethod(java.lang.String pattern, java.lang.String methodName) - 初始化指定的方法,只是当前方法不需要任何参数。
       addCallMethod(java.lang.String pattern, java.lang.String methodName, int paramCount) - 初始化指定的方法,参数个数为
       paramCount,参数类型缺省为java.lang.String。注意有种特殊情况,就是paramCount为0的时候,默认使用当前元素的字符数据作为
       参数值。
       addCallMethod(java.lang.String pattern, java.lang.String methodName, int paramCount, java.lang.Class[] paramTypes) -
       同上,只是有指定的参数类型.
       addCallMethod(java.lang.String pattern, java.lang.String methodName, int paramCount, java.lang.String[] paramTypes) -
       同上
     
     CallParamRule:提供CallMethodRule规则所需要的参数,必须跟CallMethodRule配合使用。
     对应Digester中有关这个规则的Javadoc方法说明:
       addCallParam(java.lang.String pattern, int paramIndex) - 使用匹配当前指定模式元素的字符数据作为索引为paramIndex的参数值.
       addCallParam(java.lang.String pattern, int paramIndex, boolean fromStack)   - 从操作数栈中,默认取出栈顶对象作为索引
       为paramIndex的参数值.
       addCallParam(java.lang.String pattern, int paramIndex, int stackIndex)   - 从操作数栈中,取出从栈顶数第stackIndex + 1个
       对象作为索引为paramIndex的参数值.
       addCallParam(java.lang.String pattern, int paramIndex, java.lang.String attributeName) - 使用属性attrbuteName的值作为
       索引为paramIndex的参数值.
     
     PathCallParamRule:提供当前匹配的模式路径作为方法调用所需要的参数,配合CallMethodRule使用。
     对应Digester中有关这个规则的Javadoc方法说明:
       addCallParamPath(java.lang.String pattern, int paramIndex) - 指定索引为paramIndex的值为当前匹配模式的路径.
    
     ObjectParamRule;指定对象作为指定索引的值,配合CallMethodRule使用。
     对应Digester中有关这个规则的Javadoc方法说明:
       addObjectParam(java.lang.String pattern, int paramIndex, java.lang.Object paramObj) - 指定索引为paramIndex的值为给定
       的对象的值.
     
     SetNestedPropertiesRule:当前匹配模式的直接子元素和对应bean的属性之间的映射.
     对应Digester中有关这个规则的Javadoc方法说明:
       addSetNestedProperties(java.lang.String pattern)   - 默认当前匹配模式的元素的直接子元素和bean中对应属性之间值的映射
       addSetNestedProperties(java.lang.String pattern, java.lang.String[] elementNames, java.lang.String[] propertyNames)    -
       当前匹配模式的直接子元素集和bean中属性集之间的映射
       addSetNestedProperties(java.lang.String pattern, java.lang.String elementName, java.lang.String propertyName) -
       当前匹配模式的直接子元素和bean中属性之间的映射

   SetNextRule:匹配当前模式时,将栈顶对象作为次栈顶对象中指定方法的参数。
   对应Digester中有关这个规则的Javadoc方法说明:
     addSetNext(java.lang.String pattern, java.lang.String methodName) - 指定次栈顶元素的方法名称,将栈顶对象作为指定方法的参数。
     addSetNext(java.lang.String pattern, java.lang.String methodName, java.lang.String paramType) - 指定次栈顶元素的方法名称和参数类型,将栈顶对象作为指定方法的参数。

   SetPropertiesRule:匹配当前模式的元素的属性与栈顶对象中同名或者指定对应关系的属性值。
   对应Digester中有关这个规则的Javadoc方法说明:
     addSetProperties(java.lang.String pattern) - 指定栈顶对象属性的值为当前匹配元素中同名元素属性的值。
     addSetProperties(java.lang.String pattern, java.lang.String[] attributeNames, java.lang.String[] propertyNames) - 对应栈顶对象属性的值为指定的当前匹配元素中元素属性的值
     addSetProperties(java.lang.String pattern, java.lang.String attributeName, java.lang.String propertyName)   - 同上

   SetPropertyRule:不常用的一个规则,主要用于key-value值对。一个元素属性为栈顶对象的属性,一个元素属性为栈顶对象的属性的值。
   对应Digester中有关这个规则的Javadoc方法说明:
     addSetProperty(java.lang.String pattern, java.lang.String name, java.lang.String value) - name和value都是匹配当前模式的元素属性,name的值是栈顶对象中同名的属性名称,而value的值是栈顶对象中属性名称为name的值。(好像有点绕口)

   SetRootRule:将当前栈顶对象作为根对象中指定方法的参数。
   对应Digester中有关这个规则的Javadoc方法说明:
     addSetRoot(java.lang.String pattern, java.lang.String methodName) - 将当前栈顶对象作为根对象中指定为methodName方法的参数。
     addSetRoot(java.lang.String pattern, java.lang.String methodName, java.lang.String paramType) -   同上,只是多了一个方法参数的类型

   SetTopRule:与SetNextRule正好想法,是将次栈顶元素作为栈顶元素指定方法的参数。
   对应Digester中有关这个规则的Javadoc方法说明:
     addSetTop(java.lang.String pattern, java.lang.String methodName) - 将次栈顶元素作为栈顶元素指定为methodName方法的参数。
     addSetTop(java.lang.String pattern, java.lang.String methodName, java.lang.String paramType) -   同上,只是多了一个方法参数的类型

   前面介绍的基本就是digester的常用主要用法,正常来说足够了!不过为了提供一些额外更强大的扩展,digester提供了扩展的通配符匹配规则,更强大也更方便!那就是ExtendedBaseRules,这个类扩展了基本的匹配规则RulesBase,提供了更通用的通配符匹配规则,以下简要介绍一下:
   首先说一下基本的匹配模式,有三种:
   Parent Match(可以理解为匹配子元素的精确父匹配):a/b/c/? */a/b/c/?
   Ancester Match(可以理解为匹配那种出身自一个精确序列元素的元素):a/b/* */a/b/*
   Universal Wildcard Match(可以理解为通配符匹配,都以!开头):!*a/b !a/b/? !*a/b/? !a/b/* !*/a/b/*
   Wild Match (可以理解为更通用更模糊的通配符匹配):* !*

   ?代表直接子元素
   * 代表任意的父或子元素
   ! 代表以什么什么为开头


   举个例子:
         Digester digester = new Digester();
         digester.setRules(new ExtendedBaseRules());
         digester.setValidating(false);
       
       
         digester.addObjectCreate("!*/b", BetaBean.class);
         digester.addObjectCreate("!*/a", AlphaBean.class);
         digester.addObjectCreate("root", ArrayList.class);
         digester.addSetProperties("!*" ;
         digester.addSetNext("!*/b/?", "setChild" ;
         digester.addSetNext("!*/a/?", "setChild" ;
         digester.addSetNext("!root/?", "add" ;
         ArrayList root =
             (ArrayList) digester.parse(getInputStream("Test4.xml" );
       
         assertEquals("Wrong array size", 2, root.size());
         AlphaBean one = (AlphaBean) root.get(0);
         assertTrue(one.getChild() instanceof BetaBean);
         BetaBean two = (BetaBean) one.getChild();
         assertEquals("Wrong name (1)", two.getName() , "TWO" ;
         assertTrue(two.getChild() instanceof AlphaBean);
         AlphaBean three = (AlphaBean) two.getChild();
         assertEquals("Wrong name (2)", three.getName() , "THREE" ;      
         BetaBean four = (BetaBean) root.get(1);
         assertEquals("Wrong name (3)", four.getName() , "FOUR" ;
         assertTrue(four.getChild() instanceof BetaBean);
         BetaBean five = (BetaBean) four.getChild();
         assertEquals("Wrong name (4)", five.getName() , "FIVE" ;

    Test4.xml文件:
  
     <root>
       <a name="ONE">
         <b name="TWO">
             <a name="THREE"/>
         </b>
       </a>
       <b name="FOUR">
         <b name="FIVE"/>
       </b>
     </root>



本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/jackoo/archive/2009/06/24/4294501.aspx
分享到:
评论

相关推荐

    使用Apache的Digester来解析XML文档

    Apache Digester是一个强大的Java库,专门用于将XML文档解析成对象模型。在处理XML到Java对象映射时,它提供了一种简洁的方式,避免了手动编写大量繁琐的XML解析代码。这个工具尤其适用于那些需要频繁从XML配置文件...

    Apache Commons Digester 的使用

    NULL 博文链接:https://longsy.iteye.com/blog/380360

    org.apache.commons.digester解析XML.rar

    Apache Commons Digester是一个Java库,专门用于将XML文档解析成对象结构。这个“org.apache.commons.digester解析XML.rar”压缩包包含了一个测试工程,它演示了如何使用Digester库来解析XML文件并映射到Java对象上...

    commons-digester.jar

    `commons-digester.jar`是Apache Commons项目中的一个组件,主要功能是解析XML文档并根据预定义的规则自动创建和填充Java对象。这个组件在Java应用程序中尤其有用,特别是那些需要从XML配置文件中构建复杂对象层次...

    commons-digester3-3.2-API文档-中英对照版.zip

    标签:apache、commons、digester3、jar包、java、中英对照文档; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准...

    利用commons-digester解析xml

    标题“利用commons-digester解析XML”涉及到的是Java开发中的一种处理XML文档的工具——Apache Commons Digester。这个库提供了一种方便的方式来映射XML文档结构到Java对象,从而简化了XML数据的处理过程。 Apache ...

    使用Apache_Commons_Digester

    ### 使用Apache Commons Digester开发指南 #### 概述 Apache Commons Digester 是一款基于 Java 的开源库,专门用于简化 XML 文件解析的过程。它利用 SAX(Simple API for XML)解析器来解析 XML 数据,并通过一...

    Apache xml转换java对象 Digester

    ### Apache XML转换Java对象——Digester #### 一、引言 在软件开发过程中,XML是一种常见的数据交换格式。为了方便地将XML数据转换为Java对象,Apache Commons库提供了一个强大的工具——`Digester`。本文将详细...

    commons-digester-2.0.rar源文件及jar文件

    《Apache Commons Digester 2.0:解析XML的利器》 Apache Commons Digester是一个Java库,主要用于将XML文档解析成Java对象。这个库的核心功能是根据预定义的规则(Rule)来读取XML文档,然后根据这些规则进行一...

    commons-digester3-3.2-API文档-中文版.zip

    标签:apache、commons、digester3、jar包、java、中文文档; 使用方法:解压翻译后的API文档,用浏览器打开“index.html”文件,即可纵览文档内容。 人性化翻译,文档中的代码和结构保持不变,注释和说明精准翻译,...

    rh-maven33-apache-commons-digester-1.8.1-19.13.el7.noarch.rpm

    官方离线安装包,测试可用。使用rpm -ivh [rpm完整包名] 进行安装

    commons-digester-1.7.jar.zip

    《Apache Commons Digester 1.7:解析XML的利器》 Apache Commons Digester是一个Java库,主要用于解析XML文档,并根据预定义的规则将其转换为Java对象。在标题和描述中提到的"commons-digester-1.7.jar.zip"是一个...

    commons-digester-2.1.jar.zip

    《Apache Commons Digester详解》 Apache Commons Digester是一款在Java开发中广泛使用的开源库,它提供了处理XML文档的强大工具,可以将XML数据映射到Java对象。这个库的名字"Digester"源自其功能,就像一个消化...

    commons-digester-2.0.jar.zip

    《Apache Commons Digester详解及其在Java开发中的应用》 Apache Commons Digester是一个强大的Java库,主要功能是解析XML文档,并根据预定义的规则将其转换为Java对象。在标题"commons-digester-2.0.jar.zip"中,...

    XML的解析之——使用Digester

    本文将深入探讨如何使用Apache的 Digester 库来解析XML文档,这是一款强大的工具,能够将XML数据映射到Java对象,简化了处理XML的过程。 Digester 是Apache Commons项目的一部分,它提供了一种规则驱动的方法来处理...

    digester包

    在Java开发中,处理XML数据是一项常见的任务,而Apache Commons Digester库就是这样一个强大的工具,它提供了一种高效且方便的方式来解析XML文档,并将其映射到Java对象结构中。本文将深入探讨digester包的原理、...

    commons-digester-2.1.jar

    `commons-digester-2.1.jar` 是Apache Commons项目中的一个组件,主要负责XML文档的解析和对象的创建与绑定。Apache Commons Digester库提供了一种规则驱动的方法来解析XML文档,并根据预定义的规则将XML数据映射到...

    xml解析commons-digester的demo

    Apache Commons Digester是Java中一个强大的工具,它提供了方便的方式来解析XML文档,并将解析出的数据绑定到Java对象上,实现对象的自动创建和初始化。本示例将详细介绍如何使用Apache Commons Digester进行XML解析...

    jenkins subversion 插件 2.15.4

    解决jenkins 升级后报:FATAL: org/apache/commons/digester/Digester java.lang.ClassNotFoundException: org.apache.commons.digester.Digester 的问题

    浅析Digester

    这时,Apache Commons Digester库就派上了用场。本文将深入探讨Digester的功能、工作原理以及如何在实际项目中应用。 **一、什么是Digester** Apache Commons Digester是一个用于读取XML文档并根据预定义规则自动...

Global site tag (gtag.js) - Google Analytics