Chapter 14. Using Schemas with XQuery

Using schemas can result in queries that are better optimized and tested. This chapter first provides a brief overview of XML Schema. It then explains how schemas are used with queries by importing schema definitions and taking advantage of schema-defined types.

Full coverage of XML Schema is outside the scope of this book. For detailed coverage, please see Definitive XML Schema, 2nd Edition by Priscilla Walmsley (Prentice Hall).

What Is a Schema?

A schema is used to describe the structure and data content of XML documents. Example 14-1 shows a schema that might describe our catalog.xml sample document. This schema can be used to validate the catalog document, assuring that:

  • Only approved elements and attributes are used.

  • The elements appear in the correct order.

  • All required elements are present.

  • All elements and attributes have valid values.

In addition, it can provide information to the query processor about the types of the values in the document—for example, that product numbers are integers.

Example 14-1. Schema for the product catalog (prod_nons.xsd)
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
  <xs:element name="catalog" type="CatalogType"/>
  <xs:complexType name="CatalogType">
    <xs:sequence>
      <xs:element ref="product" maxOccurs="unbounded"/>
    </xs:sequence>
  </xs:complexType>
  <xs:element name="product" type="ProductType"/>
  <xs:complexType name="ProductType">
    <xs:sequence>
      <xs:element name="number" type="xs:integer"/>
      <xs:element name="name" type="NameType"/>
      <xs:element name="colorChoices" type="ColorListType" minOccurs="0"/>
      <xs:element name="desc" minOccurs="0"/>
    </xs:sequence>
    <xs:attribute name="dept" type="xs:string"/>
  </xs:complexType>
  <xs:simpleType name="ColorListType">
    <xs:list itemType="xs:string"/>
  </xs:simpleType>
  <xs:complexType name="NameType">
    <xs:simpleContent>
      <xs:extension base="xs:string">
        <xs:attribute name="language" type="LangType"/>
      </xs:extension>
    </xs:simpleContent>
  </xs:complexType>
  <xs:simpleType name="LangType">
    <xs:restriction base="xs:string">
      <xs:enumeration value="en"/>
      <xs:enumeration value="fr"/>
    </xs:restriction>
  </xs:simpleType>
</xs:schema>

Why Use Schemas with Queries?

There are a number of advantages of using schemas with queries:

Predictability

If an input document has been validated against a schema, its structure and data content are predictable. If the schema says that there will always be a number child of product, and your input document was validated, you can be sure that a number child will exist for each product. You do not need to check for its existence before you use it in an expression.

Type information for use in expressions

The schema provides type information to the query processor about the values in the instance document. For example, it can tell us that the number element contains an integer value. This is useful, for example, if you write a query that returns results sorted by number. The query processor will know that the number values should be sorted as integers and not strings, and will therefore sort 100 after 99. If they were sorted as strings, 100 would come before 99.

Identification of query errors

Schemas can be used in static analysis to determine the expected type of an expression. Using schemas, you might discover errors in the query that were not otherwise apparent. Schemas can also help you debug your queries more quickly and easily by providing more useful error messages. To use an SQL analogy, you wouldn’t want an SQL statement that had a misspelled column name to come back with nothing instead of raising an error. Without XML schemas, this is exactly what your XQuery queries will do if you misspell an element name or specify an invalid path: return nothing.

Query optimization

The more a processor knows about the structure of the input documents, the more it can optimize access to them. For example, if a schema is present, an expression such as catalog//number is a simple matter of looking at the grandchildren of catalog and returning those named number. If no schema is present, every node of the document must be traversed to find number elements.

Special processing based on type

Type-related expressions, such as instance of and typeswitch, can be used on user-defined types in the schema. For example, you could write an expression that processes the product element differently depending on whether it is of type ShirtType, HatType, etc.

Validity of query results

A query might be designed to produce results that conform to a particular XML language, such as XHTML. Performing schema validation in the query ensures that the results are valid XHTML. If the query isn’t generating valid XHTML, the query processor may be able to pinpoint the error in your query.

Some query users are not concerned about these benefits, and they feel that using schemas adds too much complexity. For these users, it is entirely possible to use XQuery without schemas. If no schema is present, all the elements and attributes are untyped. This means that they are assigned generic types (xs:untyped and xs:untypedAtomic) that allow any content or value.

W3C XML Schema: A Brief Overview

The schema language supported directly by the XQuery recommendation is W3C XML Schema, which is a W3C Recommendation with two normative parts:

Part 1: Structures

Defines a language for defining schemas, which express constraints on XML documents.

Part 2: Datatypes

Defines a rich set of built-in types that can be applied to XML documents through schemas, as well as through other mechanisms.

The XML Schema built-in types defined in Part 2 are the basis for the built-in types used in XQuery. Regardless of whether schemas are present, these types can be used in queries to represent common datatypes such as strings, numbers, dates, and times. All XQuery implementations support this basic set of types.

XQuery also interacts with Part 1 of XML Schema. Schema definitions can be used in queries to validate input documents, intermediate values, and result elements.

XQuery does not currently provide specific support for other schema languages or validation mechanisms such as DTDs, RELAX NG, or Schematron. However, a processor can be written that validates a document by using one of these schema languages and annotates its elements and attributes with appropriate types.

Element and Attribute Declarations

Elements and attributes are the most basic components of an XML document. The catalog schema has a declaration for each of the different kinds of elements and attributes in the catalog, such as product, number, and dept. Elements are declared in the schema by using an xs:element element, while attributes are declared with xs:attribute.

Element and attribute declarations can be declared globally or locally. The catalog and product element declarations are global, meaning that they appear as a child of xs:schema in the schema document. The other element declarations, along with the attribute declaration, are local, and their scope is the type (ProductType) in which they are declared. If you’re writing a schema with XQuery in mind, you should use a global declaration for any elements that you want to validate separately. That’s because you can only start validation by using a global element declaration.

Types

Every element and attribute is associated with a type. Types are used to specify a class of elements (or attributes), including their allowed values, the structure of their content, and/or their attributes.

Simple and complex types

Types in XML Schema can be either simple or complex. Simple types are those that allow text content, but no child elements or attributes. In our catalog.xml document, the elements number and colorChoices have simple types because they have neither children nor attributes. Attributes themselves always have simple types because they always have simple values.

Complex types, by contrast, allow children and/or attributes. In catalog.xml, the elements catalog, product, and desc have complex types because they have children. The name element also has a complex type, even though it does not have children, because it can have an attribute.

Complex types are further divided into four different content types. The different content types vary in whether they allow child elements and text content. Table 14-1 lists the four content types and provides examples for each. The content type is not affected by the presence of attributes; all complex types allow attributes, regardless of content type.

Table 14-1. Content types for complex types
Content typeAllows children?Allows text content?Example
SimpleNoYes <name lang="en">Shirt</name>
Element-onlyYesNo <product dept="MEN">   <number>784</number> </product>
MixedYesYes <desc>Our <i>best</i> shirt!</desc>
EmptyNoNo <discounted value="true"/>

User-defined types

XML Schema allows user-defined types to be created based on existing types. A new simple type can be defined that is a restriction of another type. Example 14-1 shows a simple type LangType that is derived from the built-in type xs:string. New complex types can also be derived from other complex types, either by restriction (further constraining the base type) or extension (adding new attributes or child elements). For example, based on the schema in Example 14-1 you could define a new complex type ShirtType that extends ProductType to add child elements that are relevant only to shirts, such as sleeve length, and not to products in general.

As with the built-in types, these type derivations result in a type definition hierarchy that is in some ways analogous to an object-oriented hierarchy of sub- and superclasses.

List types

A list type is a different variety of simple type that represents whitespace-separated lists of values. The type of each item in the list is known as the item type. In our example schema, the colorChoices element is declared to be of a type that is a list of xs:string values. Therefore, the element:

<colorChoices>navy black</colorChoices>

has content that is treated like two separate values, navy and black. An XQuery processor treats it somewhat differently than if it had an atomic type: it treats it like a sequence of two atomic values, in this case strings. If you test the value for equality, as follows:

doc("catalog.xml")//product[colorChoices = 'navy']

it will return that element (the first product in the catalog). If colorChoices were of type xs:string, or untyped, the product would not be selected because the value navy black would be treated as one string.

Namespaces and XML Schema

When a target namespace is indicated in a schema, all of the globally declared elements and attributes take on that target namespace as part of their name. Example 14-2 shows the beginning of the catalog schema, which specifies a target namespace, http://datypic.com/prod, using the targetNamespace attribute on the xs:schema element.

Example 14-2. Beginning of the catalog schema with target namespace (prod.xsd)
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           elementFormDefault="qualified"
           targetNamespace="http://datypic.com/prod"
           xmlns:prod="http://datypic.com/prod">
  <xs:element name="catalog" type="prod:CatalogType"/>
  <xs:complexType name="CatalogType">
  <!-- ... -->
  </xs:complexType>
</xs:schema>

Using this schema, the catalog element would have to be associated with a namespace in an instance document (and in any queries on that instance document). In addition, named type definitions also have names that are qualified by the target namespace. To refer to CatalogType, you use its qualified name—for example, prod:CatalogType if the prefix prod is bound to the namespace http://datypic.com/prod.

In-Scope Schema Definitions

In any module, there is a set of in-scope schema definitions (ISSDs) that can be referenced within the query. This includes type definitions, element declarations, and attribute declarations. They can describe the input documents, the result XML, or both. You may want to reference schema definitions for a number of reasons, such as:

  • To write functions that only accept values of a certain user-defined type. For example, a function that queries sleeve length might accept only elements of type ShirtType.

  • To validate a node that you constructed in your query. For example, you used constructors to create a product element and its children, and you want to ensure that it is valid according to ProductType.

  • To determine whether a value is an instance of a particular user-defined type in order to decide how to process it. For example, if the size value is an instance of ShirtSizeType, you want to call one function, whereas if it is a value of HatSizeType, you want to call a different function.

  • You want to perform static type analysis and you want the schema definitions to be taken into account.

If you don’t need to do any of these things, you are not required to include your schemas in the in-scope schema definitions. This is true even if your input document was validated against a schema.

Where Do In-Scope Schema Definitions Come From?

Definitions for the built-in types are automatically included in the ISSD. A processor may include additional schema declarations and definitions in the ISSD according to implementation-defined rules. For example, it may have a set of built-in schemas that are always present. Processors that support schema validation may add definitions from the schema with which an input document is validated. Additionally, some implementations will allow you to specify programmatically or as a parameter—outside the scope of the query—what schemas to import.

Schema Imports

A schema import is used in XQuery to add a schema to the ISSD for a module. When a schema is imported, all of its global element and attribute declarations and type definitions are added.

A schema import, which appears in the query prolog, specifies the target namespace and, optionally, the schema location. For example:

import schema "http://datypic.com/prod"
           at "prod.xsd";

imports the schema document at prod.xsd whose target namespace is http://datypic.com/prod.

The syntax of a schema import is shown in Figure 14-1. The schema location and namespace must be literal values in quotes (not evaluated expressions).

Figure 14-1. Syntax of a schema import

For convenience, it is also possible to include a namespace declaration as part of the schema import. For example:

import schema namespace prod = "http://datypic.com/prod"
              at "prod.xsd";

binds the prod prefix to the namespace, in addition to importing it. Additionally, a default namespace declaration can be included in a schema import, as in:

import schema default element namespace "http://datypic.com/prod"
              at "prod.xsd";

This has the effect of making http://datypic.com/prod the default element namespace, in addition to importing the schema with that target namespace.

If the at keyword and schema location are left off, it is assumed that the processor knows where to locate the schema for the specified namespace. The processor is even allowed to ignore the provided location if it has another method of locating the schema.

Schema import is not supported by all implementations, only those that support the Schema Aware feature. If a schema import is used but is not supported by the implementation, error XQST0009 is raised.

Importing a schema with no target namespace

If the imported schema has no target namespace, a zero-length string should be used for the target namespace, as in:

import schema "" at "prod_nons.xsd";

In order to reference any of the element or type names in that schema, you must make the default namespace the zero-length string (""). You can do this using the same syntax described in the previous section:

import schema default element namespace ""
              at "prod_nons.xsd";

Importing multiple schemas with the same target namespace

Multiple schema imports can appear in the query prolog, but only one per namespace. To specify multiple schema documents with the same target namespace, use a single schema import with multiple schema locations separated by commas, as in:

import schema "http://datypic.com/prod"
              at "prod.xsd", "prod2.xsd";

Schema imports and library modules

When importing a library module into your query, it is important to understand that a module import only imports the function and variable declarations of the library module. It does not automatically import the schemas that are imported in the prolog of the library module.

For example, suppose you have a library module called strings.xqm that imports a schema named stringtypes.xsd. When the main module imports strings.xqm, the stringtypes.xsd definitions are not automatically added to the in-scope schema definitions of the main module. If the main module needs to refer directly to any of the types or declarations of stringtypes.xsd, it must import that schema explicitly in its prolog.

In addition, the main module must import stringtypes.xsd if it uses any variables or functions from strings.xqm that depend on a type definition from the schema. For example, suppose strings.xqm contains the variable declaration:

declare variable $strings:LetterA as strings:smallString := "A";

where smallString is a user-defined type defined in stringtypes.xsd. The main module must import the stringtypes.xsd schema if it uses the LetterA variable. This does not apply to the built-in types, such as xs:integer or xs:string, whose definitions are always in scope.

Schema Validation and Type Assignment

Adding a schema to the ISSD does not automatically cause any input documents or result XML to be validated or annotated with types. There are two occasions during query evaluation when schema validation may occur.

The first is when an input document is opened, for example, using the doc or collection function. Depending on the implementation, the processor may validate the input document at this time. However, a processor is not required to automatically validate input documents, even if it supports XML Schema. It can choose the way it finds and selects schemas for the input document. Additionally, the processor is not required to stop evaluating the query if an input document is found to be invalid but still well formed. You should consult the documentation for your XQuery implementation to determine how it handles these choices.

If you’re relying on an input document being pre-validated in this way, it’s a good idea to declare this. For example you can write:

declare variable $in
   as document-node(schema-element(catalog))
   := doc("catalog.xml");

This causes the query to fail if the validation hasn’t been done (or if validation failed). It also gives the query compiler the expected type of $in, which is useful information for optimization and error checking.

If an input document is validated, the definitions used must be consistent with any definitions added to the ISSD. For example, if your input document is a catalog.xml document that was validated using catalog.xsd, you cannot then import a different catalog schema that has conflicting definitions.

The second occasion is when a validate expression is used to explicitly validate documents and elements.

The Validate Expression

A validate expression can be used to validate a document or element node, which may come from an input document or be constructed in the query. It will validate the node according to a schema declaration if that declaration is in scope (i.e., if it is in the ISSD). For example:

validate strict { <product dept="ACC">
  <number>563</number>
  <name language="en">Floppy Sun Hat</name>
</product> }

validates the product element using a global product element declaration from the in-scope schema definitions, if one exists. This includes validating its attributes and descendants.

The syntax of a validate expression is shown in Figure 14-2.

Figure 14-2. Syntax of a validate expression

Starting in version 3.0, it is possible to specify the type name against which you wish to validate the element. In this case, you use the keyword type followed by the qualified name of the type. For example, the following validate expression will validate the product element against the type ProductType:

validate type ProductType { <product dept="ACC">
  <number>563</number>
  <name language="en">Floppy Sun Hat</name>
</product> }

The type must be among the in-scope schema definitions. If the type name is in a namespace, that namespace must be declared in the query, either as the default element namespace or using a prefix that is then used in the type name.

The expression to be validated must be either a single element node or a single document node that has exactly one element child.

The value of a validate expression is a new document or element node (with a new identity) annotated with the appropriate type indicated in the element declaration.

As with all schema validation, it also fills in default or fixed values and normalizes whitespace. When a document node is being validated, full schema validation is performed. When an element node is being validated, certain validation constraints are skipped. These omitted constraints include checking xs:ID values for uniqueness, and ensuring that xs:ENTITY, xs:NOTATION, and xs:IDREF values have matching entities, notations, and IDs.

Only implementations that support the optional Schema Aware Feature implement the validate expression. If this feature is used but is not supported by the implementation, error XQST0075 is raised.

Validation Mode

The validation mode controls how strictly an element or document is validated. There are two possible validation modes:

strict

When it is strict, the processor requires that a declaration be present for the element in the validate expression and that it be valid according to those declarations. If the element is not valid or a declaration cannot be found for it, an error is raised.

lax

When it is lax, the processor validates the element if it can find a declaration for it. It may not be able to find declarations if, for example, the schema was not imported or provided by the processor. If a declaration is found, the element or attribute must be valid according to it, or an error is raised. If no declaration is found, the processor will attempt to recursively validate the element’s children and attributes, and the process repeats. If no declarations are found in the entire tree or no validation errors are encountered, no error is raised.

In a validate expression, the validation mode can be specified just after the validate keyword. For example:

validate lax
 {<number>563</number>}

results in lax validation on the number element. If it is not specified, the default mode strict is used.

Assigning Type Annotations to Nodes

It is worth taking a closer look at how the validation process assigns type annotations to elements and attributes. If the node is valid according to the type designated in its declaration, it is usually quite straightforward. For example, the product element in the previous example would be assigned the type ProductType. However, there are some special cases.

An element or attribute is assigned a generic type (xs:untyped for elements, and xs:untypedAtomic for attributes) if:

  • No schema validation was attempted.

  • It was not validated because it was included as part of a wildcard (xs:any or xs:anyAttribute) that does not require validation.

  • It is the result of an element constructor, and construction mode is set to strip. Construction mode is described in “Types and Newly Constructed Elements and Attributes”.

  • It is the result of a validate expression, but it was not validated against an in-scope schema definition. This might happen if the validation mode is lax with no relevant declaration in scope.

Another generic type, xs:anyType, is used in a few other cases. The difference between xs:anyType and xs:untyped is that an element of type xs:anyType may contain other elements that have specific types. Elements of type xs:untyped, on the other hand, always have children that are untyped. An element is assigned the type xs:anyType if:

  • When an input document was accessed, validation was attempted but the element was found to be invalid (or partially valid). Some implementations may allow the query evaluation to continue even if validation fails.

  • It is the result of an element constructor, and construction mode is set to preserve.

A node that is declared to have a union type is assigned the specific member type for which it was validated (which is the first one to which it conforms). For example, if the <a>12</a> element is validated using a union type whose member types are xs:integer and xs:string, in that order, it is assigned the type xs:integer, not the union type itself.

An element that uses the XML Schema attribute xsi:type for type substitution is assigned the type specified by xsi:type if it is valid according to that type definition.

Nodes and Typed Values

In most cases, you can retrieve the typed value of an element or attribute by using the data function. Usually, it is simply the string value of the node, cast to the type of the element or attribute. For example, if the number element has the type xs:integer, the string value is 784 (type xs:string), while the typed value is 784 (type xs:integer). If the number element is untyped, its typed value is 784 (type xs:untypedAtomic).

There are two exceptions to this rule:

  • Elements whose types have element-only content (that is, they allow only children and not text) do not have typed values, even if that particular element does not have any children. For example, the product element does not have a typed value if it is annotated with a type other than xs:untyped.

  • The typed value of an element or attribute whose type is a list type is a sequence of atomic values, one for each item in the list. For example, if the element <colorChoices>navy black</colorChoices> has a type that is a list of strings, the typed value is a sequence of two strings, navy and black. The type of each atomic value is the item type of the list.

An element’s typed value will be the empty sequence in two cases:

  • Its type is a complex type with an empty content model.

  • It has been nilled, meaning that it has an attribute xsi:nil="true".

The typed value will not be the empty sequence just because the element has no content. For example, the typed value of <name></name> is the value "" (type xs:untypedAtomic) if name is untyped, and a zero-length string (type xs:string) if name has a complex type with mixed content but happens to be empty.

A summary of the rules for the typed values of elements and attributes appears in Table 14-2.

Table 14-2. Typed values of elements and attributes
Kind of nodeTyped valueType of typed value
An untyped elementThe character data content of the element and all its descendants xs:untypedAtomic
An element whose type is a simple type, or a complex type with simple contentThe character data content of the elementThe type of the element’s content
An element whose type has mixed contentThe character data content of the element and all its descendants xs:untypedAtomic
An element whose type has element-only contentErrorN/A
An element whose type has empty content () N/A
An untyped attributeThe attribute value xs:untypedAtomic
A typed attributeThe attribute valueThe type of the attribute
An element or attribute whose type is a list typeA sequence containing the values in the listThe list type’s item type
An element that is nilled (regardless of its type)()N/A

Types and Newly Constructed Elements and Attributes

Newly constructed nodes don’t automatically take on the type of their content. For example, the expression <abc>{2}</abc> does not create an abc element whose type annotation is xs:integer just because its content is of type xs:integer. In fact, element constructors that have simple content, as in this example, are always annotated with xs:untyped unless they are enclosed in a validate expression.

The type of a newly constructed element with complex content is also generic, but any children it copies from an input document may or may not retain their original types from the input document. This is determined by construction mode, which can have one of two values: strip or preserve. If construction mode is strip, the type of the newly constructed element, and all of its descendants, is xs:untyped. If the element is contained in a validate expression, it may then be annotated with a new schema type.

If construction mode is preserve, the type of the newly constructed element is xs:anyType, and all of its copied children retain their original types from the input document.

For example, suppose you construct a productList element with the following expression:

<productList>{doc("catalog.xml")//product}</productList>

Suppose also that the catalog.xml document has been validated with a schema, and the product elements from this document are annotated with the type ProductType. This query will result in a productList element that contains four product elements.

If construction mode is strip, both productList and all the product elements in the results will be annotated with xs:untyped. If construction mode is preserve, productList will be annotated with xs:anyType, and the product elements will be annotated with ProductType.

Construction mode is set using a construction declaration, which may appear in the query prolog. Its syntax is shown in Figure 14-3.

Figure 14-3. Syntax of a construction declaration

Sequence Types and Schemas

Chapter 11 showed how sequence types are used to match sequences in various expressions, including function calls. When schemas have been imported into a query, additional tests are available for sequence types, including testing for name and type. Their syntax is shown in Figure 14-4. These tests can be used not just in sequence types but also as node kind tests in path expressions.

Figure 14-4. Element and attribute tests (for sequence types and kind tests)

“Element and Attribute Tests” introduced the element() and attribute() tests. For example, you can use the test element(prod:product) to test for elements whose name is prod:product.

These tests can also be used with user-defined types. For example, the sequence type:

element(prod:product, prod:ProductType)

matches an element named prod:product that has the type prod:ProductType, or any type derived by restriction or extension from prod:ProductType. Yet another syntax is:

element(*, prod:ProductType)

which matches any element that has the type prod:ProductType (or a derived type), regardless of name. Note that the element must already have been validated and annotated with the type prod:ProductType. It is not enough that it would be a valid instance of that type if it were validated.

You can also match an element or attribute based on its name by using the schema-element() and schema-attribute() tests. For example, you can use the sequence type schema-element(prod:product) to match only elements whose qualified name is prod:product. This differs from the element(prod:product) syntax in that the name must be among the globally declared element or attribute names in the ISSD. Also, in order to match, a node must have been validated according to that declaration.

Another difference is that schema-element(prod:product) will also match elements that are in the substitution group of product. Substitution groups are a feature of XML Schema that allows you to specify that certain elements are equivalent. For example, you might put elements shirt, hat, and suitcase in the substitution group headed by the product element. These three elements can then appear in content anywhere a product element may appear. The sequence type schema-element(prod:product) would then match shirt, hat, and suitcase elements in addition to product elements.

For attributes, you can specify schema-attribute and attribute, and the same rules apply. However, these constructs are rarely used because attributes don’t have substitution groups, and global attribute declarations are quite rare. Table 14-3 shows some examples.

Table 14-3. Examples of sequence types based on name and type
ExampleExplanation
schema-element(product)+ One or more validated elements whose qualified name is product, that have been validated using a global element declaration in the ISSD, or a member of the substitution group of that declaration
schema-element(prod:product) One element whose qualified name is equal to prod:product, that has been validated using a global element declaration in the ISSD, or a member of the substitution group of that declaration
schema-attribute(prod:dept) One attribute whose qualified name is prod:dept, that has been validated using a global attribute declaration in the ISSD
element(*, prod:ProductType) One element whose type annotation is prod:ProductType or a type derived from prod:ProductType
element(prod:product, prod:ProductType) One element whose qualified name is equal to prod:product, whose type annotation is prod:ProductType or a type derived from prod:ProductType, that is in the ISSD

You can use a question mark at the end of the type name in an element sequence type. This means that if an element is nilled (i.e., it has the attribute xsi:nil set to true), it can match the sequence type even though it does not have any content. For example, element(product, ProductType?) matches both a regular product element and a nilled product element such as <product xsi:nil="true"/>. Note that this is different from a question mark at the very end of the sequence type, which indicates that the empty sequence should match.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.22.27.45