dbaccess/CppunitTest_dbaccess_SingleSelectQueryComposer_test.mk |   51 -
 dbaccess/JunitTest_dbaccess_complex.mk                          |    1 
 dbaccess/Module_dbaccess.mk                                     |    1 
 dbaccess/qa/complex/dbaccess/SingleSelectQueryComposer.java     |  355 +++++++
 dbaccess/qa/unit/SingleSelectQueryComposer_test.cxx             |  448 
----------
 5 files changed, 356 insertions(+), 500 deletions(-)

New commits:
commit 42190cd153d4061a8401acdb2c2f8edf9154a978
Author:     Noel Grandin <noel.gran...@collabora.co.uk>
AuthorDate: Mon Mar 31 12:38:35 2025 +0200
Commit:     Noel Grandin <noel.gran...@collabora.co.uk>
CommitDate: Mon Mar 31 18:08:26 2025 +0200

    Revert "Move SingleSelectQueryComposer.java tests to C++"
    
    This reverts commit d1ec7e90772421331bef31eb934180c7f992e42f.
    
    To avoid deadlocks on jenkins.
    
    Change-Id: Icbe299ed29dfd02f61b14953344a902e2184c606
    Reviewed-on: https://gerrit.libreoffice.org/c/core/+/183535
    Tested-by: Jenkins
    Reviewed-by: Noel Grandin <noel.gran...@collabora.co.uk>

diff --git a/dbaccess/CppunitTest_dbaccess_SingleSelectQueryComposer_test.mk 
b/dbaccess/CppunitTest_dbaccess_SingleSelectQueryComposer_test.mk
deleted file mode 100644
index 7be6e2dbbd37..000000000000
--- a/dbaccess/CppunitTest_dbaccess_SingleSelectQueryComposer_test.mk
+++ /dev/null
@@ -1,51 +0,0 @@
-# -*- Mode: makefile-gmake; tab-width: 4; indent-tabs-mode: t -*-
-#
-# This file is part of the LibreOffice project.
-#
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-
-$(eval $(call 
gb_CppunitTest_CppunitTest,dbaccess_SingleSelectQueryComposer_test))
-
-$(eval $(call 
gb_CppunitTest_use_external,dbaccess_SingleSelectQueryComposer_test,boost_headers))
-
-$(eval $(call 
gb_CppunitTest_add_exception_objects,dbaccess_SingleSelectQueryComposer_test, \
-    dbaccess/qa/unit/SingleSelectQueryComposer_test \
-))
-
-$(eval $(call 
gb_CppunitTest_use_libraries,dbaccess_SingleSelectQueryComposer_test, \
-    comphelper \
-    cppu \
-    cppuhelper \
-    dbaxml \
-    dbtools \
-    sal \
-    subsequenttest \
-    utl \
-    test \
-    tk \
-    tl \
-    unotest \
-    xo \
-))
-
-$(eval $(call gb_CppunitTest_use_api,dbaccess_SingleSelectQueryComposer_test,\
-    offapi \
-    oovbaapi \
-    udkapi \
-))
-
-$(eval $(call gb_CppunitTest_use_ure,dbaccess_SingleSelectQueryComposer_test))
-$(eval $(call gb_CppunitTest_use_vcl,dbaccess_SingleSelectQueryComposer_test))
-
-$(eval $(call 
gb_CppunitTest_use_rdb,dbaccess_SingleSelectQueryComposer_test,services))
-
-$(eval $(call 
gb_CppunitTest_use_configuration,dbaccess_SingleSelectQueryComposer_test))
-
-$(eval $(call 
gb_CppunitTest_use_uiconfigs,dbaccess_SingleSelectQueryComposer_test, \
-    dbaccess \
-))
-
-# vim: set noet sw=4 ts=4:
diff --git a/dbaccess/JunitTest_dbaccess_complex.mk 
b/dbaccess/JunitTest_dbaccess_complex.mk
index bed3b5231355..04609c4413f0 100644
--- a/dbaccess/JunitTest_dbaccess_complex.mk
+++ b/dbaccess/JunitTest_dbaccess_complex.mk
@@ -41,6 +41,7 @@ $(eval $(call gb_JunitTest_add_sourcefiles,dbaccess_complex,\
     dbaccess/qa/complex/dbaccess/QueryInQuery \
     dbaccess/qa/complex/dbaccess/RowSet \
     dbaccess/qa/complex/dbaccess/RowSetEventListener \
+    dbaccess/qa/complex/dbaccess/SingleSelectQueryComposer \
     dbaccess/qa/complex/dbaccess/TestCase \
     dbaccess/qa/complex/dbaccess/UISettings \
 ))
diff --git a/dbaccess/Module_dbaccess.mk b/dbaccess/Module_dbaccess.mk
index 960989e5443c..f368fcc33e19 100644
--- a/dbaccess/Module_dbaccess.mk
+++ b/dbaccess/Module_dbaccess.mk
@@ -59,7 +59,6 @@ $(eval $(call gb_Module_add_check_targets,dbaccess,\
     CppunitTest_dbaccess_hsqldb_test \
     CppunitTest_dbaccess_RowSetClones \
     CppunitTest_dbaccess_CRMDatabase_test \
-    CppunitTest_dbaccess_SingleSelectQueryComposer_test \
 ))
 endif
 
diff --git a/dbaccess/qa/complex/dbaccess/SingleSelectQueryComposer.java 
b/dbaccess/qa/complex/dbaccess/SingleSelectQueryComposer.java
new file mode 100644
index 000000000000..978f318cafef
--- /dev/null
+++ b/dbaccess/qa/complex/dbaccess/SingleSelectQueryComposer.java
@@ -0,0 +1,355 @@
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ *
+ * This file incorporates work covered by the following license notice:
+ *
+ *   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 .
+ */
+package complex.dbaccess;
+
+import com.sun.star.beans.PropertyState;
+import com.sun.star.sdb.SQLFilterOperator;
+import com.sun.star.beans.PropertyAttribute;
+import com.sun.star.beans.XPropertySet;
+import com.sun.star.beans.XPropertyContainer;
+import com.sun.star.beans.NamedValue;
+import com.sun.star.container.XNameAccess;
+import com.sun.star.sdbcx.XTablesSupplier;
+import com.sun.star.sdb.XParametersSupplier;
+import com.sun.star.beans.PropertyValue;
+import com.sun.star.sdbcx.XColumnsSupplier;
+import com.sun.star.container.XIndexAccess;
+import com.sun.star.sdb.CommandType;
+import com.sun.star.sdb.XSingleSelectQueryComposer;
+import com.sun.star.uno.UnoRuntime;
+import com.sun.star.sdbc.DataType;
+import com.sun.star.sdbc.SQLException;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+
+// ---------- junit imports -----------------
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ Note that this test CANNOT BE CONVERTED TO C++
+ because the JDBC driver uses a STA/AFFINE UNO compartment thing, which is 
extremely deadlock prone
+ when called from inside the same process.
+*/
+public class SingleSelectQueryComposer extends CRMBasedTestCase
+{
+
+    private XSingleSelectQueryComposer m_composer = null;
+    private static final String COMPLEXFILTER = "( \"ID\" = 1 AND \"Postal\" = 
'4' )"
+            + " OR ( \"ID\" = 2 AND \"Postal\" = '5' )"
+            + " OR ( \"ID\" = 3 AND \"Postal\" = '6' AND \"Address\" = '7' )"
+            + " OR ( \"Address\" = '8' )"
+            + " OR ( \"Postal\" = '9' )"
+            + " OR ( NOW( ) = {d '2010-01-01' } )";
+    private static final String INNERPRODUCTSQUERY = "products (inner)";
+
+
+    private void createQueries() throws Exception
+    {
+        
m_database.getDatabase().getDataSource().createQuery(INNERPRODUCTSQUERY, 
"SELECT * FROM \"products\"");
+    }
+
+
+    @Override
+    protected void createTestCase() throws Exception
+    {
+        super.createTestCase();
+
+        createQueries();
+
+        m_composer = createQueryComposer();
+    }
+
+
+    private void checkAttributeAccess(String _attributeName, String 
_attributeValue)
+    {
+        System.out.println("setting " + _attributeName + " to " + 
_attributeValue);
+        String realValue = null;
+        try
+        {
+            final Class<?> composerClass = m_composer.getClass();
+            final Method attributeGetter = composerClass.getMethod("get" + 
_attributeName, new Class[]
+                    {
+                    });
+            final Method attributeSetter = composerClass.getMethod("set" + 
_attributeName, new Class[]
+                    {
+                        String.class
+                    });
+
+            attributeSetter.invoke(m_composer, new Object[]
+                    {
+                        _attributeValue
+                    });
+            realValue = (String) attributeGetter.invoke(m_composer, new 
Object[]
+                    {
+                    });
+        }
+        catch (NoSuchMethodException e)
+        {
+        }
+        catch (IllegalAccessException e)
+        {
+        }
+        catch (InvocationTargetException e)
+        {
+        }
+        assertTrue("set/get" + _attributeName + " not working as expected 
(set: " + _attributeValue + ", get: " + (realValue != null ? realValue : 
"null") + ")",
+                realValue.equals(_attributeValue));
+        System.out.println("  (results in " + m_composer.getQuery() + ")");
+    }
+
+    /** tests setCommand of the composer
+     */
+    @Test
+    public void testSetCommand() throws Exception
+    {
+        System.out.println("testing SingleSelectQueryComposer's setCommand");
+
+        final String table = "SELECT * FROM \"customers\"";
+        m_composer.setCommand("customers", CommandType.TABLE);
+        assertTrue("setCommand/getQuery TABLE inconsistent", 
m_composer.getQuery().equals(table));
+
+        m_database.getDatabase().getDataSource().createQuery("set command 
test", "SELECT * FROM \"orders for customer\" \"a\", \"customers\" \"b\" WHERE 
\"a\".\"Product Name\" = \"b\".\"Name\"");
+        m_composer.setCommand("set command test", CommandType.QUERY);
+        assertTrue("setCommand/getQuery QUERY inconsistent", 
m_composer.getQuery().equals(m_database.getDatabase().getDataSource().getQueryDefinition("set
 command test").getCommand()));
+
+        final String sql = "SELECT * FROM \"orders for customer\" WHERE 
\"Product Name\" = 'test'";
+        m_composer.setCommand(sql, CommandType.COMMAND);
+        assertTrue("setCommand/getQuery COMMAND inconsistent", 
m_composer.getQuery().equals(sql));
+    }
+
+    /** tests accessing attributes of the composer (order, filter, group by, 
having)
+     */
+    @Test
+    public void testAttributes() throws Exception
+    {
+        System.out.println("testing SingleSelectQueryComposer's attributes 
(order, filter, group by, having)");
+
+        System.out.println("check setElementaryQuery");
+
+        final String simpleQuery2 = "SELECT * FROM \"customers\" WHERE 
\"Name\" = 'oranges'";
+        m_composer.setElementaryQuery(simpleQuery2);
+        assertTrue("setElementaryQuery/getQuery inconsistent", 
m_composer.getQuery().equals(simpleQuery2));
+
+        System.out.println("check setQuery");
+        final String simpleQuery = "SELECT * FROM \"customers\"";
+        m_composer.setQuery(simpleQuery);
+        assertTrue("set/getQuery inconsistent", 
m_composer.getQuery().equals(simpleQuery));
+
+        checkAttributeAccess("Filter", "\"Name\" = 'oranges'");
+        checkAttributeAccess("Group", "\"City\"");
+        checkAttributeAccess("Order", "\"Address\"");
+        checkAttributeAccess("HavingClause", "\"ID\" <> 4");
+
+        final XIndexAccess orderColumns = m_composer.getOrderColumns();
+        assertTrue("Order columns doesn't exist: \"Address\"",
+        orderColumns != null && orderColumns.getCount() == 1 && 
orderColumns.getByIndex(0) != null);
+
+        final XIndexAccess groupColumns = m_composer.getGroupColumns();
+        assertTrue("Group columns doesn't exist: \"City\"",
+        groupColumns != null && groupColumns.getCount() == 1 && 
groupColumns.getByIndex(0) != null);
+
+        // XColumnsSupplier
+        final XColumnsSupplier xSelectColumns = 
UnoRuntime.queryInterface(XColumnsSupplier.class, m_composer);
+        assertTrue("no select columns, or wrong number of select columns",
+        xSelectColumns != null && xSelectColumns.getColumns() != null && 
xSelectColumns.getColumns().getElementNames().length == 6);
+
+        // structured filter
+        m_composer.setQuery("SELECT \"ID\", \"Postal\", \"Address\" FROM 
\"customers\"");
+        m_composer.setFilter(COMPLEXFILTER);
+        final PropertyValue[][] aStructuredFilter = 
m_composer.getStructuredFilter();
+        m_composer.setFilter("");
+        m_composer.setStructuredFilter(aStructuredFilter);
+        if (!m_composer.getFilter().equals(COMPLEXFILTER))
+        {
+            System.out.println(COMPLEXFILTER);
+            System.out.println(m_composer.getFilter());
+        }
+        assertTrue("Structured Filter not identical", 
m_composer.getFilter().equals(COMPLEXFILTER));
+
+        // structured having clause
+        m_composer.setHavingClause(COMPLEXFILTER);
+        final PropertyValue[][] aStructuredHaving = 
m_composer.getStructuredHavingClause();
+        m_composer.setHavingClause("");
+        m_composer.setStructuredHavingClause(aStructuredHaving);
+        assertTrue("Structured Having Clause not identical", 
m_composer.getHavingClause().equals(COMPLEXFILTER));
+    }
+
+    /** test various sub query related features ("queries in queries")
+     */
+    @Test
+    public void testSubQueries() throws Exception
+    {
+        m_composer.setQuery("SELECT * from \"" + INNERPRODUCTSQUERY + "\"");
+        final XTablesSupplier suppTables = 
UnoRuntime.queryInterface(XTablesSupplier.class, m_composer);
+        final XNameAccess tables = suppTables.getTables();
+        assertTrue("a simple SELECT * FROM <query> could not be parsed",
+                tables != null && tables.hasByName(INNERPRODUCTSQUERY));
+
+        final String sInnerCommand = 
m_database.getDatabase().getDataSource().getQueryDefinition(INNERPRODUCTSQUERY).getCommand();
+        final String sExecutableQuery = m_composer.getQueryWithSubstitution();
+        assertTrue("simple query containing a sub query improperly parsed to 
SDBC level statement: 
1. " + sExecutableQuery + "
2. " + "SELECT * FROM ( " + sInnerCommand + " ) AS \"" + INNERPRODUCTSQUERY + 
"\"",
+                sExecutableQuery.equals("SELECT * FROM ( " + sInnerCommand + " 
) AS \"" + INNERPRODUCTSQUERY + "\""));
+    }
+
+    /** tests the XParametersSupplier functionality
+     */
+    @Test
+    public void testParameters() throws Exception
+    {
+        // "orders for customers" is a query with a named parameter (based on 
another query)
+        m_database.getDatabase().getDataSource().createQuery("orders for 
customer", "SELECT * FROM \"all orders\" WHERE \"Customer Name\" LIKE :cname");
+        // "orders for customer and product" is query based on "orders for 
customers", adding an additional,
+        // anonymous parameter
+        m_database.getDatabase().getDataSource().createQuery("orders for 
customer and product", "SELECT * FROM \"orders for customer\" WHERE \"Product 
Name\" LIKE ?");
+
+        
m_composer.setQuery(m_database.getDatabase().getDataSource().getQueryDefinition("orders
 for customer and product").getCommand());
+        final XParametersSupplier suppParams = 
UnoRuntime.queryInterface(XParametersSupplier.class, m_composer);
+        final XIndexAccess parameters = suppParams.getParameters();
+
+        final String expectedParamNames[] =
+
+        {
+            "cname",
+            "Product Name"
+        };
+
+        final int paramCount = parameters.getCount();
+        assertTrue("composer did find wrong number of parameters in the nested 
queries.",
+                paramCount == expectedParamNames.length);
+
+        for (int i = 0; i < paramCount; ++i)
+        {
+            final XPropertySet parameter = 
UnoRuntime.queryInterface(XPropertySet.class, parameters.getByIndex(i));
+            final String paramName = (String) 
parameter.getPropertyValue("Name");
+            assertTrue("wrong parameter name at position " + (i + 1) + " 
(expected: " + expectedParamNames[i] + ", found: " + paramName + ")",
+                    paramName.equals(expectedParamNames[i]));
+
+        }
+    }
+
+    @Test
+    public void testConditionByColumn() throws Exception
+    {
+        m_composer.setQuery("SELECT * FROM \"customers\"");
+
+        final Object initArgs[] =
+
+        {
+            new NamedValue("AutomaticAddition", Boolean.TRUE)
+        };
+        final String serviceName = "com.sun.star.beans.PropertyBag";
+        final XPropertyContainer filter = 
UnoRuntime.queryInterface(XPropertyContainer.class, 
getMSF().createInstanceWithArguments(serviceName, initArgs));
+        filter.addProperty("Name", PropertyAttribute.MAYBEVOID, "Comment");
+        filter.addProperty("RealName", PropertyAttribute.MAYBEVOID, "Comment");
+        filter.addProperty("TableName", PropertyAttribute.MAYBEVOID, 
"customers");
+        filter.addProperty("Value", PropertyAttribute.MAYBEVOID, "Good one.");
+        filter.addProperty("Type", PropertyAttribute.MAYBEVOID, 
Integer.valueOf(DataType.LONGVARCHAR));
+        final XPropertySet column = 
UnoRuntime.queryInterface(XPropertySet.class, filter);
+
+        m_composer.appendFilterByColumn(column, true, SQLFilterOperator.LIKE);
+        assertTrue("At least one row should exist", 
m_database.getConnection().createStatement().executeQuery(m_composer.getQuery()).next());
+    }
+
+    private void impl_testDisjunctiveNormalForm(String _query, 
PropertyValue[][] _expectedDNF) throws SQLException
+    {
+        m_composer.setQuery(_query);
+
+        final PropertyValue[][] disjunctiveNormalForm = 
m_composer.getStructuredFilter();
+
+        assertEquals("DNF: wrong number of rows", _expectedDNF.length, 
disjunctiveNormalForm.length);
+        for (int i = 0; i < _expectedDNF.length; ++i)
+        {
+            assertEquals("DNF: wrong number of columns in row " + i, 
_expectedDNF[i].length, disjunctiveNormalForm[i].length);
+            for (int j = 0; j < _expectedDNF[i].length; ++j)
+            {
+                assertEquals("DNF: wrong content in column " + j + ", row " + 
i,
+                        _expectedDNF[i][j].Name, 
disjunctiveNormalForm[i][j].Name);
+            }
+        }
+    }
+
+    /** tests the disjunctive normal form functionality, aka the structured 
filter,
+     *  of the composer
+     */
+    @Test
+    public void testDisjunctiveNormalForm() throws Exception
+    {
+        // a simple case: WHERE clause simply is a combination of predicates 
knitted with AND
+        String query =
+                "SELECT \"customers\".\"Name\", "
+                + "\"customers\".\"Address\", "
+                + "\"customers\".\"City\", "
+                + "\"customers\".\"Postal\", "
+                + "\"products\".\"Name\" "
+                + "FROM \"orders\", \"customers\", \"orders_details\", 
\"products\" "
+                + "WHERE (   \"orders\".\"CustomerID\" = \"customers\".\"ID\" "
+                + "AND \"orders_details\".\"OrderID\" = \"orders\".\"ID\" "
+                + "AND \"orders_details\".\"ProductID\" = \"products\".\"ID\" "
+                + ") ";
+
+        impl_testDisjunctiveNormalForm(query, new PropertyValue[][]
+                {
+                    new PropertyValue[]
+                    {
+                        new PropertyValue("CustomerID", 
SQLFilterOperator.EQUAL, "\"customers\".\"ID\"", PropertyState.DIRECT_VALUE),
+                        new PropertyValue("OrderID", SQLFilterOperator.EQUAL, 
"\"orders\".\"ID\"", PropertyState.DIRECT_VALUE),
+                        new PropertyValue("ProductID", 
SQLFilterOperator.EQUAL, "\"products\".\"ID\"", PropertyState.DIRECT_VALUE)
+                    }
+                });
+
+        // somewhat more challenging: One of the conjunction terms is a 
disjunction itself
+        query =
+                "SELECT \"customers\".\"Name\", "
+                + "\"customers\".\"Address\", "
+                + "\"customers\".\"City\", "
+                + "\"customers\".\"Postal\", "
+                + "\"products\".\"Name\" "
+                + "FROM \"orders\", \"customers\", \"orders_details\", 
\"products\" "
+                + "WHERE (   \"orders\".\"CustomerID\" = \"customers\".\"ID\" "
+                + "AND \"orders_details\".\"OrderID\" = \"orders\".\"ID\" "
+                + "AND \"orders_details\".\"ProductID\" = \"products\".\"ID\" "
+                + ") "
+                + "AND "
+                + "(  \"products\".\"Name\" = 'Apples' "
+                + "OR \"products\".\"ID\" = 2 "
+                + ")";
+
+        impl_testDisjunctiveNormalForm(query, new PropertyValue[][]
+                {
+                    new PropertyValue[]
+                    {
+                        new PropertyValue("CustomerID", 
SQLFilterOperator.EQUAL, "\"customers\".\"ID\"", PropertyState.DIRECT_VALUE),
+                        new PropertyValue("OrderID", SQLFilterOperator.EQUAL, 
"\"orders\".\"ID\"", PropertyState.DIRECT_VALUE),
+                        new PropertyValue("ProductID", 
SQLFilterOperator.EQUAL, "\"products\".\"ID\"", PropertyState.DIRECT_VALUE),
+                        new PropertyValue("Name", SQLFilterOperator.EQUAL, 
"Apples", PropertyState.DIRECT_VALUE)
+                    },
+                    new PropertyValue[]
+                    {
+                        new PropertyValue("CustomerID", 
SQLFilterOperator.EQUAL, "\"customers\".\"ID\"", PropertyState.DIRECT_VALUE),
+                        new PropertyValue("OrderID", SQLFilterOperator.EQUAL, 
"\"orders\".\"ID\"", PropertyState.DIRECT_VALUE),
+                        new PropertyValue("ProductID", 
SQLFilterOperator.EQUAL, "\"products\".\"ID\"", PropertyState.DIRECT_VALUE),
+                        new PropertyValue("ID", SQLFilterOperator.EQUAL, 
Integer.valueOf(2), PropertyState.DIRECT_VALUE)
+                    }
+                });
+
+    }
+}
diff --git a/dbaccess/qa/unit/SingleSelectQueryComposer_test.cxx 
b/dbaccess/qa/unit/SingleSelectQueryComposer_test.cxx
deleted file mode 100644
index fab287ce7a62..000000000000
--- a/dbaccess/qa/unit/SingleSelectQueryComposer_test.cxx
+++ /dev/null
@@ -1,448 +0,0 @@
-/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
-/*
- * This file is part of the LibreOffice project.
- *
- * This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/.
- */
-
-#include "dbtest_base.cxx"
-
-#include <com/sun/star/beans/NamedValue.hpp>
-#include <com/sun/star/beans/PropertyAttribute.hpp>
-#include <com/sun/star/beans/XPropertyContainer.hpp>
-#include <com/sun/star/container/XNameAccess.hpp>
-#include <com/sun/star/lang/XMultiServiceFactory.hpp>
-
-#include <com/sun/star/sdb/CommandType.hpp>
-#include <com/sun/star/sdb/SQLFilterOperator.hpp>
-#include <com/sun/star/sdb/XOfficeDatabaseDocument.hpp>
-#include <com/sun/star/sdb/XQueriesSupplier.hpp>
-#include <com/sun/star/sdb/XQueryDefinition.hpp>
-#include <com/sun/star/sdb/XQueryDefinitionsSupplier.hpp>
-#include <com/sun/star/sdb/XSingleSelectQueryComposer.hpp>
-#include <com/sun/star/sdb/XParametersSupplier.hpp>
-#include <com/sun/star/sdbc/DataType.hpp>
-#include <com/sun/star/sdbc/XDataSource.hpp>
-#include <com/sun/star/sdbc/XRow.hpp>
-#include <com/sun/star/sdbc/XResultSet.hpp>
-#include <com/sun/star/sdbcx/XColumnsSupplier.hpp>
-#include <com/sun/star/sdbcx/XTablesSupplier.hpp>
-
-using namespace ::com::sun::star;
-using namespace ::com::sun::star::uno;
-
-class SingleSelectQueryComposerTest : public DBTestBase
-{
-    Reference<XDataSource> m_xDataSource;
-    Reference<XConnection> m_xConnection;
-    Reference<XSingleSelectQueryComposer> m_xComposer;
-
-public:
-    virtual void setUp() override;
-    virtual void tearDown() override;
-    void testSetCommand();
-    void testAttributes();
-    void testSubQueries();
-    void testParameters();
-    void testConditionByColumn();
-    void testDisjunctiveNormalForm();
-
-    CPPUNIT_TEST_SUITE(SingleSelectQueryComposerTest);
-    CPPUNIT_TEST(testSetCommand);
-    CPPUNIT_TEST(testAttributes);
-    CPPUNIT_TEST(testSubQueries);
-    CPPUNIT_TEST(testParameters);
-    CPPUNIT_TEST(testConditionByColumn);
-    CPPUNIT_TEST(testDisjunctiveNormalForm);
-    CPPUNIT_TEST_SUITE_END();
-};
-
-void SingleSelectQueryComposerTest::setUp()
-{
-    DBTestBase::setUp();
-    createDBDocument(u"sdbc:embedded:hsqldb"_ustr);
-    Reference<sdb::XOfficeDatabaseDocument> xDocument(mxComponent, 
UNO_QUERY_THROW);
-    m_xDataSource = xDocument->getDataSource();
-
-    // Create some common queries
-    DBTestBase::createQueries(m_xDataSource);
-
-    m_xConnection = m_xDataSource->getConnection(u""_ustr, u""_ustr);
-    DBTestBase::createTables(m_xConnection);
-
-    // For some reason we must close, then reopen the connection in
-    // order for the tables to be visible to the connection
-    m_xConnection->close();
-    m_xConnection = m_xDataSource->getConnection(u""_ustr, u""_ustr);
-
-    Reference<lang::XMultiServiceFactory> xFactory(m_xConnection, UNO_QUERY);
-    Reference<XSingleSelectQueryComposer> xComposer(
-        
xFactory->createInstance(u"com.sun.star.sdb.SingleSelectQueryComposer"_ustr),
-        UNO_QUERY_THROW);
-    m_xComposer = xComposer;
-}
-
-void SingleSelectQueryComposerTest::tearDown()
-{
-    m_xComposer.clear();
-    if (m_xConnection)
-    {
-        m_xConnection->close();
-        m_xConnection.clear();
-    }
-    DBTestBase::tearDown();
-}
-
-// tests setCommand of the composer
-
-void SingleSelectQueryComposerTest::testSetCommand()
-{
-    // CommandType::TABLE
-    m_xComposer->setCommand(u"CUSTOMERS"_ustr, CommandType::TABLE);
-    CPPUNIT_ASSERT_EQUAL(u"SELECT * FROM \"CUSTOMERS\""_ustr, 
m_xComposer->getQuery());
-
-    // CommandType::QUERY
-    DBTestBase::createQuery(u"SELECT * FROM \"orders for customer\" \"a\", 
\"customers\" "
-                            "\"b\" WHERE \"a\".\"Product Name\" = 
\"b\".\"Name\""_ustr,
-                            true, u"set command test"_ustr, m_xDataSource);
-
-    Reference<XQueriesSupplier> xQuerySupplier(m_xConnection, UNO_QUERY_THROW);
-    Reference<container::XNameAccess> xQueryAccess = 
xQuerySupplier->getQueries();
-    CPPUNIT_ASSERT(xQueryAccess->hasElements());
-    Reference<XPropertySet> xQuery(xQueryAccess->getByName(u"set command 
test"_ustr), UNO_QUERY);
-    CPPUNIT_ASSERT(xQuery.is());
-
-    m_xComposer->setCommand(u"set command test"_ustr, CommandType::QUERY);
-    OUString sQuery;
-    xQuery->getPropertyValue(u"Command"_ustr) >>= sQuery;
-
-    CPPUNIT_ASSERT_EQUAL(sQuery, m_xComposer->getQuery());
-
-    // CommandType::COMMAND
-    const OUString sCommand
-        = u"SELECT * FROM \"orders for customer\" WHERE \"Product Name\" = 
'test'"_ustr;
-    m_xComposer->setCommand(sCommand, CommandType::COMMAND);
-    CPPUNIT_ASSERT_EQUAL(sCommand, m_xComposer->getQuery());
-}
-
-// tests accessing attributes of the composer (order, filter, group by, having)
-
-void SingleSelectQueryComposerTest::testAttributes()
-{
-    const OUString sSimpleQuery2 = u"SELECT * FROM \"CUSTOMERS\" WHERE 
\"Name\" = 'oranges'"_ustr;
-    m_xComposer->setElementaryQuery(sSimpleQuery2);
-    CPPUNIT_ASSERT_EQUAL(sSimpleQuery2, m_xComposer->getQuery());
-
-    const OUString sSimpleQuery = u"SELECT * FROM \"CUSTOMERS\""_ustr;
-    m_xComposer->setQuery(sSimpleQuery);
-    CPPUNIT_ASSERT_EQUAL(sSimpleQuery, m_xComposer->getQuery());
-
-    // checkAttributeAccess "Filter"
-    OUString sFilter = u"\"NAME\" = 'oranges'"_ustr;
-    m_xComposer->setFilter(sFilter);
-    CPPUNIT_ASSERT_EQUAL(sFilter, m_xComposer->getFilter());
-
-    // checkAttributeAccess "Group"
-    sFilter = u"\"CITY\""_ustr;
-    m_xComposer->setGroup(sFilter);
-    CPPUNIT_ASSERT_EQUAL(sFilter, m_xComposer->getGroup());
-
-    // checkAttributeAccess "Order"
-    sFilter = u"\"ADDRESS\""_ustr;
-    m_xComposer->setOrder(sFilter);
-    CPPUNIT_ASSERT_EQUAL(sFilter, m_xComposer->getOrder());
-
-    // checkAttributeAccess "HavingClause"
-    sFilter = u"\"ID\" <> 4"_ustr;
-    m_xComposer->setHavingClause(sFilter);
-    CPPUNIT_ASSERT_EQUAL(sFilter, m_xComposer->getHavingClause());
-
-    // check getOrderColumns
-    Reference<container::XIndexAccess> 
orderColumns(m_xComposer->getOrderColumns(), UNO_SET_THROW);
-    CPPUNIT_ASSERT(orderColumns->hasElements());
-    CPPUNIT_ASSERT_EQUAL(sal_Int32(1), orderColumns->getCount());
-
-    const Reference<XPropertySet> xOrderColumns(orderColumns->getByIndex(0), 
UNO_QUERY_THROW);
-    OUString sColumnName;
-    xOrderColumns->getPropertyValue(u"Name"_ustr) >>= sColumnName;
-    CPPUNIT_ASSERT_EQUAL(u"ADDRESS"_ustr, sColumnName);
-
-    // check getGroupColumns
-    Reference<container::XIndexAccess> 
groupColumns(m_xComposer->getGroupColumns(), UNO_SET_THROW);
-    CPPUNIT_ASSERT(groupColumns->hasElements());
-    CPPUNIT_ASSERT_EQUAL(sal_Int32(1), groupColumns->getCount());
-
-    const Reference<XPropertySet> xGroupColumns(groupColumns->getByIndex(0), 
UNO_QUERY_THROW);
-    OUString sGroupColumnName;
-    xGroupColumns->getPropertyValue(u"Name"_ustr) >>= sGroupColumnName;
-    CPPUNIT_ASSERT_EQUAL(u"CITY"_ustr, sGroupColumnName);
-
-    // XColumnSupplier
-    Reference<sdbcx::XColumnsSupplier> xSelectColumns(m_xComposer, 
UNO_QUERY_THROW);
-    Reference<container::XNameAccess> xColumnAccess = 
xSelectColumns->getColumns();
-    CPPUNIT_ASSERT(xColumnAccess->hasElements());
-    CPPUNIT_ASSERT_EQUAL(sal_Int32(6), 
xColumnAccess->getElementNames().getLength());
-
-    const OUString COMPLEXFILTER = u"( \"ID\" = 1 AND \"POSTAL\" = '4' )"
-                                   " OR ( \"ID\" = 2 AND \"POSTAL\" = '5' )"
-                                   " OR ( \"ID\" = 3 AND \"POSTAL\" = '6' AND 
\"ADDRESS\" = '7' )"
-                                   " OR ( \"ADDRESS\" = '8' )"
-                                   " OR ( \"POSTAL\" = '9' )"
-                                   " OR ( NOW( ) = {d '2010-01-01' } )"_ustr;
-
-    // structured filter
-    m_xComposer->setQuery(u"SELECT \"ID\", \"POSTAL\", \"ADDRESS\" FROM 
\"CUSTOMERS\""_ustr);
-    m_xComposer->setFilter(COMPLEXFILTER);
-    Sequence<Sequence<PropertyValue>> aStructuredFilter = 
m_xComposer->getStructuredFilter();
-    m_xComposer->setFilter(u""_ustr);
-    m_xComposer->setStructuredFilter(aStructuredFilter);
-    CPPUNIT_ASSERT_EQUAL(COMPLEXFILTER, m_xComposer->getFilter());
-
-    // structured having clause
-    m_xComposer->setHavingClause(COMPLEXFILTER);
-    Sequence<Sequence<PropertyValue>> aStructuredHaving = 
m_xComposer->getStructuredHavingClause();
-    m_xComposer->setHavingClause("");
-    m_xComposer->setStructuredHavingClause(aStructuredHaving);
-    CPPUNIT_ASSERT_EQUAL(COMPLEXFILTER, m_xComposer->getHavingClause());
-}
-
-// test various sub query related features ("queries in queries")
-
-void SingleSelectQueryComposerTest::testSubQueries()
-{
-    const OUString INNERPRODUCTSQUERY = u"products (inner)"_ustr;
-    DBTestBase::createQuery(u"SELECT * FROM \"PRODUCTS\""_ustr, true, 
INNERPRODUCTSQUERY,
-                            m_xDataSource);
-
-    m_xComposer->setQuery(u"SELECT * FROM \""_ustr + INNERPRODUCTSQUERY + 
u"\""_ustr);
-
-    Reference<sdbcx::XTablesSupplier> suppTables(m_xComposer, UNO_QUERY_THROW);
-    Reference<container::XNameAccess> tables = suppTables->getTables();
-    CPPUNIT_ASSERT(tables->hasElements());
-    CPPUNIT_ASSERT(tables->hasByName(INNERPRODUCTSQUERY));
-
-    Reference<XQueriesSupplier> xQuerySupplier(m_xConnection, UNO_QUERY_THROW);
-    Reference<container::XNameAccess> xQueryAccess = 
xQuerySupplier->getQueries();
-    CPPUNIT_ASSERT(xQueryAccess->hasElements());
-
-    Reference<XPropertySet> 
xQuery(xQueryAccess->getByName(INNERPRODUCTSQUERY), UNO_QUERY);
-    OUString sInnerProductsQuery;
-    xQuery->getPropertyValue(u"Command"_ustr) >>= sInnerProductsQuery;
-
-    const OUString sExpectedQuery = u"SELECT * FROM ( "_ustr + 
sInnerProductsQuery
-                                    + u" )"
-                                      " AS \""_ustr
-                                    + INNERPRODUCTSQUERY + u"\""_ustr;
-    const OUString sExecutableQuery = m_xComposer->getQueryWithSubstitution();
-
-    CPPUNIT_ASSERT_EQUAL(sExpectedQuery, sExecutableQuery);
-}
-
-// test the XParametersSupplier functionality
-
-void SingleSelectQueryComposerTest::testParameters()
-{
-    // "orders for customers" is a query with a named parameter (based on 
another query)
-    DBTestBase::createQuery(
-        u"SELECT * FROM \"orders for customer\" WHERE \"Product Name\" LIKE 
?"_ustr, true,
-        u"orders for customer and product"_ustr, m_xDataSource);
-
-    // "orders for customer and product" is query based on "orders for 
customers", adding an additional,
-    DBTestBase::createQuery(
-        u"SELECT * FROM \"all orders\" WHERE \"Customer Name\" LIKE 
:cname"_ustr, true,
-        u"orders for customer"_ustr, m_xDataSource);
-
-    Reference<XQueryDefinitionsSupplier> xQuerySupplier(m_xDataSource, 
UNO_QUERY);
-    Reference<container::XNameAccess> xQueryAccess = 
xQuerySupplier->getQueryDefinitions();
-    Reference<sdb::XQueryDefinition> xQueryDefinition(
-        xQueryAccess->getByName(u"orders for customer and product"_ustr), 
UNO_QUERY);
-
-    OUString sCustomersAndProduct;
-    xQueryDefinition->getPropertyValue(u"Command"_ustr) >>= 
sCustomersAndProduct;
-
-    m_xComposer->setQuery(sCustomersAndProduct);
-
-    Reference<XParametersSupplier> xSuppParams(m_xComposer, UNO_QUERY_THROW);
-    Reference<container::XIndexAccess> xParameters = 
xSuppParams->getParameters();
-
-    Sequence<OUString> const expectedParameters{ u"cname"_ustr, u"Product 
Name"_ustr };
-    CPPUNIT_ASSERT_EQUAL(expectedParameters.getLength(), 
xParameters->getCount());
-
-    for (auto i = 0; i < expectedParameters.getLength(); ++i)
-    {
-        Reference<XPropertySet> xParam(xParameters->getByIndex(i), UNO_QUERY);
-        OUString sParamName;
-        xParam->getPropertyValue(u"Name"_ustr) >>= sParamName;
-        CPPUNIT_ASSERT_EQUAL(expectedParameters[i], sParamName);
-    }
-}
-
-void SingleSelectQueryComposerTest::testConditionByColumn()
-{
-    m_xComposer->setQuery("SELECT * FROM \"CUSTOMERS\"");
-    Sequence<Any> aArgs{ Any(NamedValue(u"AutomaticAddition"_ustr, Any(true))) 
};
-    Reference<beans::XPropertyContainer> filter(
-        
m_xSFactory->createInstanceWithArguments(u"com.sun.star.beans.PropertyBag"_ustr,
 aArgs),
-        UNO_QUERY);
-
-    filter->addProperty(u"Name"_ustr, PropertyAttribute::MAYBEVOID, 
Any(u"COMMENT"_ustr));
-    filter->addProperty(u"RealName"_ustr, PropertyAttribute::MAYBEVOID, 
Any(u"COMMENT"_ustr));
-    filter->addProperty(u"TableName"_ustr, PropertyAttribute::MAYBEVOID, 
Any(u"CUSTOMERS"_ustr));
-    filter->addProperty(u"Value"_ustr, PropertyAttribute::MAYBEVOID, 
Any(u"Good one."_ustr));
-    filter->addProperty(u"Type"_ustr, PropertyAttribute::MAYBEVOID,
-                        Any(sal_Int32(sdbc::DataType::LONGVARCHAR)));
-
-    Reference<XPropertySet> xColumn(filter, UNO_QUERY_THROW);
-    m_xComposer->appendFilterByColumn(xColumn, true, SQLFilterOperator::LIKE);
-
-    Reference<XStatement> xStatement = m_xConnection->createStatement();
-    Reference<XResultSet> xResults = 
xStatement->executeQuery(m_xComposer->getQuery());
-    CPPUNIT_ASSERT(xResults.is());
-
-    // At least one row should exist
-    Reference<XRow> xRow(xResults, UNO_QUERY_THROW);
-    CPPUNIT_ASSERT(xResults->next());
-    // The row returned should be the 4th, i.e. the one that
-    // contains the Value "Good One"
-    CPPUNIT_ASSERT_EQUAL(u"4"_ustr, xRow->getString(1));
-}
-
-// tests the disjunctive normal form functionality, aka the structured filter,
-// of the composer
-
-void SingleSelectQueryComposerTest::testDisjunctiveNormalForm()
-{
-    // a simple case: WHERE clause simply is a combination of predicates 
knitted with AND
-    OUString sQuery = u"SELECT \"CUSTOMERS\".\"NAME\", "
-                      "\"CUSTOMERS\".\"ADDRESS\", "
-                      "\"CUSTOMERS\".\"CITY\", "
-                      "\"CUSTOMERS\".\"POSTAL\", "
-                      "\"PRODUCTS\".\"NAME\" "
-                      "FROM \"ORDERS\", \"CUSTOMERS\", \"ORDERS_DETAILS\", 
\"PRODUCTS\" "
-                      "WHERE (   \"ORDERS\".\"CUSTOMERID\" = 
\"CUSTOMERS\".\"ID\" "
-                      "AND \"ORDERS_DETAILS\".\"ORDERID\" = \"ORDERS\".\"ID\" "
-                      "AND \"ORDERS_DETAILS\".\"PRODUCTID\" = 
\"PRODUCTS\".\"ID\" "
-                      ") "_ustr;
-
-    m_xComposer->setQuery(sQuery);
-
-    {
-        Sequence<Sequence<PropertyValue>> disjunctiveNormalForm
-            = m_xComposer->getStructuredFilter();
-
-        Sequence<PropertyValue> expectedDNF(3);
-        PropertyValue* pExpectedDNF = expectedDNF.getArray();
-
-        pExpectedDNF[0].Name = u"CUSTOMERID"_ustr;
-        pExpectedDNF[0].Handle = SQLFilterOperator::EQUAL;
-        pExpectedDNF[0].Value <<= u"\"CUSTOMERS\".\"ID\""_ustr;
-        pExpectedDNF[0].State = PropertyState_DIRECT_VALUE;
-
-        pExpectedDNF[1].Name = u"ORDERID"_ustr;
-        pExpectedDNF[1].Handle = SQLFilterOperator::EQUAL;
-        pExpectedDNF[1].Value <<= u"\"ORDER\".\"ID\""_ustr;
-        pExpectedDNF[1].State = PropertyState_DIRECT_VALUE;
-
-        pExpectedDNF[2].Name = u"PRODUCTID"_ustr;
-        pExpectedDNF[2].Handle = SQLFilterOperator::EQUAL;
-        pExpectedDNF[2].Value <<= u"\"PRODUCT\".\"ID\""_ustr;
-        pExpectedDNF[2].State = PropertyState_DIRECT_VALUE;
-
-        CPPUNIT_ASSERT_EQUAL(sal_Int32(1), disjunctiveNormalForm.getLength());
-
-        for (auto i = 0; i < expectedDNF.getLength(); ++i)
-        {
-            CPPUNIT_ASSERT_EQUAL(expectedDNF[i].Name, 
disjunctiveNormalForm[0][i].Name);
-        }
-    }
-
-    // somewhat more challenging: One of the conjunction terms is a 
disjunction itself
-    sQuery = u"SELECT \"CUSTOMERS\".\"NAME\", "
-             "\"CUSTOMERS\".\"ADDRESS\", "
-             "\"CUSTOMERS\".\"CITY\", "
-             "\"CUSTOMERS\".\"POSTAL\", "
-             "\"PRODUCTS\".\"NAME\" "
-             "FROM \"ORDERS\", \"CUSTOMERS\", \"ORDERS_DETAILS\", \"PRODUCTS\" 
"
-             "WHERE (   \"ORDERS\".\"CUSTOMERID\" = \"CUSTOMERS\".\"ID\" "
-             "AND \"ORDERS_DETAILS\".\"ORDERID\" = \"ORDERS\".\"ID\" "
-             "AND \"ORDERS_DETAILS\".\"PRODUCTID\" = \"PRODUCTS\".\"ID\" "
-             ") "
-             "AND "
-             "(  \"PRODUCTS\".\"Name\" = 'Apples' "
-             "OR \"products\".\"ID\" = 2 "
-             ")"_ustr;
-
-    m_xComposer->setQuery(sQuery);
-    Sequence<Sequence<PropertyValue>> disjunctiveNormalForm = 
m_xComposer->getStructuredFilter();
-
-    {
-        Sequence<PropertyValue> firstExpectedDNF(4);
-        PropertyValue* pFirstExpectedDNF = firstExpectedDNF.getArray();
-
-        pFirstExpectedDNF[0].Name = u"CUSTOMERID"_ustr;
-        pFirstExpectedDNF[0].Handle = SQLFilterOperator::EQUAL;
-        pFirstExpectedDNF[0].Value <<= u"\"CUSTOMERS\".\"ID\""_ustr;
-        pFirstExpectedDNF[0].State = PropertyState_DIRECT_VALUE;
-
-        pFirstExpectedDNF[1].Name = u"ORDERID"_ustr;
-        pFirstExpectedDNF[1].Handle = SQLFilterOperator::EQUAL;
-        pFirstExpectedDNF[1].Value <<= u"\"ORDER\".\"ID\""_ustr;
-        pFirstExpectedDNF[1].State = PropertyState_DIRECT_VALUE;
-
-        pFirstExpectedDNF[2].Name = u"PRODUCTID"_ustr;
-        pFirstExpectedDNF[2].Handle = SQLFilterOperator::EQUAL;
-        pFirstExpectedDNF[2].Value <<= u"\"PRODUCT\".\"ID\""_ustr;
-        pFirstExpectedDNF[2].State = PropertyState_DIRECT_VALUE;
-
-        pFirstExpectedDNF[3].Name = u"Name"_ustr;
-        pFirstExpectedDNF[3].Handle = SQLFilterOperator::EQUAL;
-        pFirstExpectedDNF[3].Value <<= u"\"Apples\""_ustr;
-        pFirstExpectedDNF[3].State = PropertyState_DIRECT_VALUE;
-
-        Sequence<PropertyValue> secondExpectedDNF(4);
-        PropertyValue* pSecondExpectedDNF = secondExpectedDNF.getArray();
-
-        pSecondExpectedDNF[0].Name = u"CUSTOMERID"_ustr;
-        pSecondExpectedDNF[0].Handle = SQLFilterOperator::EQUAL;
-        pSecondExpectedDNF[0].Value <<= u"\"CUSTOMERS\".\"ID\""_ustr;
-        pSecondExpectedDNF[0].State = PropertyState_DIRECT_VALUE;
-
-        pSecondExpectedDNF[1].Name = u"ORDERID"_ustr;
-        pSecondExpectedDNF[1].Handle = SQLFilterOperator::EQUAL;
-        pSecondExpectedDNF[1].Value <<= u"\"ORDER\".\"ID\""_ustr;
-        pSecondExpectedDNF[1].State = PropertyState_DIRECT_VALUE;
-
-        pSecondExpectedDNF[2].Name = u"PRODUCTID"_ustr;
-        pSecondExpectedDNF[2].Handle = SQLFilterOperator::EQUAL;
-        pSecondExpectedDNF[2].Value <<= u"\"PRODUCT\".\"ID\""_ustr;
-        pSecondExpectedDNF[2].State = PropertyState_DIRECT_VALUE;
-
-        pSecondExpectedDNF[3].Name = u"ID"_ustr;
-        pSecondExpectedDNF[3].Handle = SQLFilterOperator::EQUAL;
-        pSecondExpectedDNF[3].Value <<= sal_Int32(2);
-        pSecondExpectedDNF[3].State = PropertyState_DIRECT_VALUE;
-
-        Sequence<Sequence<PropertyValue>> expectedDNF(2);
-        Sequence<PropertyValue>* pExpectedDNF = expectedDNF.getArray();
-        pExpectedDNF[0] = firstExpectedDNF;
-        pExpectedDNF[1] = secondExpectedDNF;
-
-        CPPUNIT_ASSERT_EQUAL(expectedDNF.getLength(), 
disjunctiveNormalForm.getLength());
-
-        for (auto i = 0; i < expectedDNF.getLength(); ++i)
-        {
-            for (auto j = 0; j < expectedDNF[0].getLength(); ++j)
-            {
-                CPPUNIT_ASSERT_EQUAL(expectedDNF[i][j].Name, 
disjunctiveNormalForm[i][j].Name);
-            }
-        }
-    }
-}
-
-CPPUNIT_TEST_SUITE_REGISTRATION(SingleSelectQueryComposerTest);
-
-CPPUNIT_PLUGIN_IMPLEMENT();
-
-/* vim:set shiftwidth=4 softtabstop=4 expandtab: */

Reply via email to