From 270c01b95c4b0208f65d9a3a2d3e9ac50b06a76b Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 06 Oct 2016 09:12:10 +0000
Subject: [PATCH] OPENDJ-2860: support JSON syntaxes and matching rules in the server

---
 opendj-server-legacy/src/main/java/org/opends/server/schema/CoreSchemaProvider.java                              |   17 ++
 opendj-server-legacy/src/main/java/org/opends/server/schema/JsonSchemaProvider.java                              |  152 +++++++++++++++++++
 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CoreSchemaConfiguration.xml |   40 ++++
 opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JsonSchemaConfiguration.xml |  160 ++++++++++++++++++++
 opendj-server-legacy/resource/schema/02-config.ldif                                                              |   45 +++++
 opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaHandler.java                                   |    8 
 6 files changed, 416 insertions(+), 6 deletions(-)

diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CoreSchemaConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CoreSchemaConfiguration.xml
index 442f3f3..2490c42 100644
--- a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CoreSchemaConfiguration.xml
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/CoreSchemaConfiguration.xml
@@ -18,8 +18,7 @@
   extends="schema-provider"
   package="org.forgerock.opendj.server.config"
   xmlns:adm="http://opendj.forgerock.org/admin"
-  xmlns:ldap="http://opendj.forgerock.org/admin-ldap"
-  xmlns:cli="http://opendj.forgerock.org/admin-cli">
+  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
   <adm:synopsis>
     <adm:user-friendly-name />
     define the core schema elements to load.
@@ -260,4 +259,41 @@
       </ldap:attribute>
     </adm:profile>
   </adm:property>
+  <adm:property name="json-validation-policy" advanced="true">
+    <adm:synopsis>
+      Specifies the policy that will be used when validating JSON syntax values.
+    </adm:synopsis>
+    <adm:default-behavior>
+      <adm:defined>
+        <adm:value>strict</adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:enumeration>
+        <adm:value name="disabled">
+          <adm:synopsis>
+            JSON syntax values will not be validated and, as a result any
+            sequence of bytes will be acceptable.
+          </adm:synopsis>
+        </adm:value>
+        <adm:value name="lenient">
+          <adm:synopsis>
+            JSON syntax values must comply with RFC 7159 except: 1) comments are
+            allowed, 2) single quotes may be used instead of double quotes,
+            and 3) unquoted control characters are allowed in strings.
+          </adm:synopsis>
+        </adm:value>
+        <adm:value name="strict">
+          <adm:synopsis>
+            JSON syntax values must strictly conform to RFC 7159.
+          </adm:synopsis>
+        </adm:value>
+      </adm:enumeration>
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:name>ds-cfg-json-validation-policy</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
 </adm:managed-object>
diff --git a/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JsonSchemaConfiguration.xml b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JsonSchemaConfiguration.xml
new file mode 100644
index 0000000..09051eb
--- /dev/null
+++ b/opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/JsonSchemaConfiguration.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  The contents of this file are subject to the terms of the Common Development and
+  Distribution License (the License). You may not use this file except in compliance with the
+  License.
+
+  You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+  specific language governing permission and limitations under the License.
+
+  When distributing Covered Software, include this CDDL Header Notice in each file and include
+  the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+  Header, with the fields enclosed by brackets [] replaced by your own identifying
+  information: "Portions copyright [year] [name of copyright owner]".
+
+  Copyright 2016 ForgeRock AS.
+  -->
+<adm:managed-object name="json-schema" plural-name="json-schemas"
+  extends="schema-provider"
+  package="org.forgerock.opendj.server.config"
+  xmlns:adm="http://opendj.forgerock.org/admin"
+  xmlns:ldap="http://opendj.forgerock.org/admin-ldap">
+  <adm:synopsis>
+    The JSON Schema Provider provides the ability to configure customized JSON query
+    matching rules.
+  </adm:synopsis>
+  <adm:description>
+    The core schema provides a default 'jsonQueryMatch' equality matching rule for
+    JSON values which match JSON strings according to the LDAP 'caseIgnoreMatch'
+    semantics (i.e trim white space and ignore case differences), as well as the
+    indexing of all JSON fields.
+
+    This schema provider allows users to create custom JSON matching rules which
+    may use different string matching semantics and, more importantly, may only
+    index a restricted set of JSON fields, thereby consuming less backend resources.
+  </adm:description>
+  <adm:profile name="ldap">
+    <ldap:object-class>
+      <ldap:name>ds-cfg-json-schema</ldap:name>
+      <ldap:superior>ds-cfg-schema-provider</ldap:superior>
+    </ldap:object-class>
+  </adm:profile>
+  <adm:property-override name="java-class" advanced="true">
+    <adm:default-behavior>
+      <adm:defined>
+        <adm:value>
+          org.opends.server.schema.JsonSchemaProvider
+        </adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+  </adm:property-override>
+  <adm:property name="matching-rule-oid" mandatory="true">
+    <adm:synopsis>
+      The numeric OID of the custom JSON matching rule.
+    </adm:synopsis>
+    <adm:syntax>
+      <adm:string>
+        <adm:pattern>
+          <adm:regex>^([0-9.]+\\d)$</adm:regex>
+          <adm:usage>OID</adm:usage>
+          <adm:synopsis>
+            The OID of the matching rule.
+          </adm:synopsis>
+        </adm:pattern>
+      </adm:string>
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:name>ds-cfg-matching-rule-oid</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+  <adm:property name="matching-rule-name">
+    <adm:synopsis>
+      The name of the custom JSON matching rule.
+    </adm:synopsis>
+    <adm:default-behavior>
+      <adm:alias>
+        <adm:synopsis>The matching rule will not have a name.</adm:synopsis>
+      </adm:alias>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:string />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:name>ds-cfg-matching-rule-name</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+ <adm:property name="case-sensitive-strings">
+    <adm:synopsis>
+      Indicates whether JSON string comparisons should be case-sensitive.
+    </adm:synopsis>
+    <adm:default-behavior>
+      <adm:defined>
+        <adm:value>false</adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:boolean />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:name>ds-cfg-case-sensitive-strings</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+  <adm:property name="ignore-white-space">
+    <adm:synopsis>
+      Indicates whether JSON string comparisons should ignore white-space.
+    </adm:synopsis>
+    <adm:description>
+      When enabled all leading and trailing white space will be removed and
+      intermediate white space will be reduced to a single character.
+    </adm:description>
+    <adm:default-behavior>
+      <adm:defined>
+        <adm:value>true</adm:value>
+      </adm:defined>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:boolean />
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:name>ds-cfg-ignore-white-space</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+  <adm:property name="indexed-field" multi-valued="true">
+    <adm:synopsis>
+      Specifies which JSON fields should be indexed.
+    </adm:synopsis>
+    <adm:description>
+      A field will be indexed if it matches any of the configured field patterns.
+    </adm:description>
+    <adm:default-behavior>
+      <adm:alias>
+        <adm:synopsis>All JSON fields will be indexed.</adm:synopsis>
+      </adm:alias>
+    </adm:default-behavior>
+    <adm:syntax>
+      <adm:string>
+        <adm:pattern>
+          <adm:regex>.*</adm:regex>
+          <adm:usage>PATTERN</adm:usage>
+          <adm:synopsis>
+            A JSON pointer which may include wild-cards. A single '*' wild-card matches at most a single path
+            element, whereas a double '**' matches zero or more path elements.
+          </adm:synopsis>
+        </adm:pattern>
+      </adm:string>
+    </adm:syntax>
+    <adm:profile name="ldap">
+      <ldap:attribute>
+        <ldap:name>ds-cfg-indexed-field</ldap:name>
+      </ldap:attribute>
+    </adm:profile>
+  </adm:property>
+</adm:managed-object>
diff --git a/opendj-server-legacy/resource/schema/02-config.ldif b/opendj-server-legacy/resource/schema/02-config.ldif
index 8bdd3b4..4d09cbf 100644
--- a/opendj-server-legacy/resource/schema/02-config.ldif
+++ b/opendj-server-legacy/resource/schema/02-config.ldif
@@ -3962,12 +3962,47 @@
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
   SINGLE-VALUE
   X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.196
+  NAME 'ds-cfg-json-validation-policy'
+  EQUALITY caseIgnoreMatch
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+  SINGLE-VALUE
+  X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.197
+  NAME 'ds-cfg-matching-rule-oid'
+  EQUALITY caseIgnoreMatch
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+  SINGLE-VALUE
+  X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.198
+  NAME 'ds-cfg-matching-rule-name'
+  EQUALITY caseIgnoreMatch
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+  SINGLE-VALUE
+  X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.199
+  NAME 'ds-cfg-case-sensitive-strings'
+  EQUALITY booleanMatch
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+  SINGLE-VALUE
+  X-ORIGIN 'OpenDJ Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.200
+  NAME 'ds-cfg-ignore-white-space'
+  EQUALITY booleanMatch
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
+  SINGLE-VALUE
+  X-ORIGIN 'OpenDJ Directory Server' )
 attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.201
   NAME 'ds-cfg-show-subordinate-naming-contexts'
   EQUALITY booleanMatch
   SYNTAX 1.3.6.1.4.1.1466.115.121.1.7
   SINGLE-VALUE
   X-ORIGIN 'OpenDS Directory Server' )
+attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.202
+  NAME 'ds-cfg-indexed-field'
+  EQUALITY caseExactMatch
+  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
+  X-ORIGIN 'OpenDJ Directory Server' )
 objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
   NAME 'ds-cfg-access-control-handler'
   SUP top
@@ -6038,3 +6073,13 @@
   MAY ( ds-cfg-rotation-policy $
         ds-cfg-retention-policy )
   X-ORIGIN 'OpenDJ Directory Server' )
+objectClasses: ( 1.3.6.1.4.1.36733.2.1.2.53
+  NAME 'ds-cfg-json-schema'
+  SUP ds-cfg-schema-provider
+  STRUCTURAL
+  MUST ds-cfg-matching-rule-oid
+  MAY ( ds-cfg-matching-rule-name $
+        ds-cfg-case-sensitive-strings $
+        ds-cfg-ignore-white-space $
+        ds-cfg-indexed-field )
+  X-ORIGIN 'OpenDJ Directory Server' )
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/schema/CoreSchemaProvider.java b/opendj-server-legacy/src/main/java/org/opends/server/schema/CoreSchemaProvider.java
index f2459b6..7dbc0ba 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/schema/CoreSchemaProvider.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/schema/CoreSchemaProvider.java
@@ -16,6 +16,10 @@
 package org.opends.server.schema;
 
 import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.VALIDATION_POLICY;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.ValidationPolicy.DISABLED;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.ValidationPolicy.LENIENT;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.ValidationPolicy.STRICT;
 
 import java.util.List;
 
@@ -76,6 +80,19 @@
       .setOption(ALLOW_NON_STANDARD_TELEPHONE_NUMBERS, !configuration.isStrictFormatTelephoneNumbers())
       .setOption(ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX, configuration.isAllowAttributeTypesWithNoSupOrSyntax());
 
+    switch (configuration.getJsonValidationPolicy())
+    {
+    case DISABLED:
+      schemaBuilder.setOption(VALIDATION_POLICY, DISABLED);
+      break;
+    case LENIENT:
+      schemaBuilder.setOption(VALIDATION_POLICY, LENIENT);
+      break;
+    case STRICT:
+      schemaBuilder.setOption(VALIDATION_POLICY, STRICT);
+      break;
+    }
+
     for (final String oid : configuration.getDisabledMatchingRule())
     {
       if (!oid.equals(NONE_ELEMENT))
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/schema/JsonSchemaProvider.java b/opendj-server-legacy/src/main/java/org/opends/server/schema/JsonSchemaProvider.java
new file mode 100644
index 0000000..4d4219d
--- /dev/null
+++ b/opendj-server-legacy/src/main/java/org/opends/server/schema/JsonSchemaProvider.java
@@ -0,0 +1,152 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2016 ForgeRock AS.
+ */
+package org.opends.server.schema;
+
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.CASE_SENSITIVE_STRINGS;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.IGNORE_WHITE_SPACE;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.INDEXED_FIELD_PATTERNS;
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.newJsonQueryEqualityMatchingRuleImpl;
+import static org.forgerock.util.Options.defaultOptions;
+
+import java.util.List;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.config.server.ConfigChangeResult;
+import org.forgerock.opendj.config.server.ConfigException;
+import org.forgerock.opendj.config.server.ConfigurationChangeListener;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+import org.forgerock.opendj.ldap.schema.MatchingRuleImpl;
+import org.forgerock.opendj.ldap.schema.SchemaBuilder;
+import org.forgerock.opendj.rest2ldap.schema.JsonSchema;
+import org.forgerock.opendj.server.config.server.JsonSchemaCfg;
+import org.forgerock.util.Options;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ServerContext;
+import org.opends.server.schema.SchemaHandler.SchemaUpdater;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.InitializationException;
+
+/** Allows users to configure custom JSON matching rules and indexing. */
+public class JsonSchemaProvider implements SchemaProvider<JsonSchemaCfg>, ConfigurationChangeListener<JsonSchemaCfg>
+{
+  /** The current configuration of JSON schema. */
+  private JsonSchemaCfg currentConfig;
+  private ServerContext serverContext;
+
+  @Override
+  public void initialize(final ServerContext serverContext, final JsonSchemaCfg configuration,
+                         final SchemaBuilder initialSchemaBuilder) throws ConfigException, InitializationException
+  {
+    this.serverContext = serverContext;
+    this.currentConfig = configuration;
+
+    addCustomJsonMatchingRule(initialSchemaBuilder, configuration);
+    currentConfig.addJsonSchemaChangeListener(this);
+  }
+
+  private void addCustomJsonMatchingRule(final SchemaBuilder schemaBuilder, final JsonSchemaCfg configuration)
+  {
+    if (!configuration.isEnabled())
+    {
+      return;
+    }
+
+    final String nameOrOid = configuration.getMatchingRuleName() != null
+            ? configuration.getMatchingRuleName() : configuration.getMatchingRuleOid();
+    final Options options = defaultOptions().set(CASE_SENSITIVE_STRINGS, configuration.isCaseSensitiveStrings())
+                                            .set(IGNORE_WHITE_SPACE, configuration.isIgnoreWhiteSpace())
+                                            .set(INDEXED_FIELD_PATTERNS, configuration.getIndexedField());
+    final MatchingRuleImpl matchingRuleImpl = newJsonQueryEqualityMatchingRuleImpl(nameOrOid, options);
+    final MatchingRule.Builder builder = schemaBuilder.buildMatchingRule(configuration.getMatchingRuleOid())
+                                                      .syntaxOID(JsonSchema.getJsonQuerySyntax().getOID())
+                                                      .implementation(matchingRuleImpl);
+    if (configuration.getMatchingRuleName() != null)
+    {
+      builder.names(configuration.getMatchingRuleName());
+    }
+    // Let users overwrite core matching rule definitions in order to control indexing.
+    builder.addToSchemaOverwrite();
+  }
+
+  @Override
+  public void finalizeProvider()
+  {
+    if (currentConfig.isEnabled())
+    {
+      try
+      {
+        serverContext.getSchemaHandler().updateSchema(new SchemaUpdater()
+        {
+          @Override
+          public void update(SchemaBuilder builder)
+          {
+            builder.removeMatchingRule(currentConfig.getMatchingRuleOid());
+          }
+        });
+      }
+      catch (DirectoryException e)
+      {
+        // Ignore.
+      }
+    }
+    currentConfig.removeJsonSchemaChangeListener(this);
+  }
+
+  @Override
+  public boolean isConfigurationAcceptable(final JsonSchemaCfg configuration,
+                                           final List<LocalizableMessage> unacceptableReasons)
+  {
+    return isConfigurationChangeAcceptable(configuration, unacceptableReasons);
+  }
+
+  @Override
+  public boolean isConfigurationChangeAcceptable(final JsonSchemaCfg configuration,
+                                                 final List<LocalizableMessage> unacceptableReasons)
+  {
+    return true;
+  }
+
+  @Override
+  public ConfigChangeResult applyConfigurationChange(final JsonSchemaCfg configuration)
+  {
+    final ConfigChangeResult ccr = new ConfigChangeResult();
+    try
+    {
+      serverContext.getSchemaHandler().updateSchema(new SchemaUpdater()
+      {
+        @Override
+        public void update(SchemaBuilder builder)
+        {
+          if (currentConfig.isEnabled())
+          {
+            builder.removeMatchingRule(currentConfig.getMatchingRuleOid());
+          }
+          addCustomJsonMatchingRule(builder, configuration);
+        }
+      });
+    }
+    catch (DirectoryException e)
+    {
+      ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
+      ccr.addMessage(e.getMessageObject());
+    }
+    finally
+    {
+      currentConfig = configuration;
+    }
+    return ccr;
+  }
+}
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaHandler.java
index 354cf36..378d73f 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/schema/SchemaHandler.java
@@ -15,6 +15,7 @@
  */
 package org.opends.server.schema;
 
+import static org.forgerock.opendj.rest2ldap.schema.JsonSchema.addJsonSyntaxesAndMatchingRulesToSchema;
 import static org.opends.server.util.SchemaUtils.is02ConfigLdif;
 
 import static java.util.Collections.emptyList;
@@ -209,10 +210,10 @@
       // Start from the core schema
       final SchemaBuilder schemaBuilder = new SchemaBuilder(Schema.getCoreSchema());
 
-      loadSchemaFromProviders(serverContext.getRootConfig(), schemaBuilder);
-
+      // Load core syntaxes and matching rules first then let providers adjust them if needed.
       addServerSyntaxesAndMatchingRules(schemaBuilder);
 
+      loadSchemaFromProviders(serverContext.getRootConfig(), schemaBuilder);
       loadSchemaFromFiles(schemaBuilder);
 
       try
@@ -244,6 +245,7 @@
       addHistoricalCsnOrderingMatchingRule(schemaBuilder);
       addAuthPasswordEqualityMatchingRule(schemaBuilder);
       addUserPasswordEqualityMatchingRule(schemaBuilder);
+      addJsonSyntaxesAndMatchingRulesToSchema(schemaBuilder);
     }
     catch (ConflictingSchemaElementException e)
     {
@@ -705,8 +707,6 @@
    *          The root to retrieve schema provider configurations.
    * @param schemaBuilder
    *          The schema builder that providers should update.
-   * @param schemaUpdater
-   *          The updater that providers should use when applying a configuration change.
    */
   private void loadSchemaFromProviders(final RootCfg rootConfiguration, final SchemaBuilder schemaBuilder)
       throws ConfigException, InitializationException {

--
Gitblit v1.10.0