Skip to content

Commit

Permalink
Bug/115 drop virtual schema with misconfigured output (#209)
Browse files Browse the repository at this point in the history
* #115: `DROP VIRTUAL SCHEMA` now works even if debug output is misconfigured
  • Loading branch information
redcatbear authored Jun 24, 2019
1 parent dae3149 commit c71a928
Show file tree
Hide file tree
Showing 26 changed files with 163 additions and 146 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,5 @@ Running the Virtual Schema requires a Java Runtime version 8 or later.
| [Java Hamcrest](http://hamcrest.org/JavaHamcrest/) | Checking for conditions in code via matchers | BSD License |
| [JSONassert](http://jsonassert.skyscreamer.org/) | Compare JSON documents for semantic equality | Apache License 2.0 |
| [JUnit](https://junit.org/junit5) | Unit testing framework | Eclipse Public License 1.0 |
| [JUnit 5 System Extensions](https://github.com/itsallcode/junit5-system-extensions) | Capturing `STDOUT` and `STDERR` | Eclipse Public License 2.0 |
| [Mockito](http://site.mockito.org/) | Mocking framework | MIT License |
| [SnakeYaml](https://bitbucket.org/asomov/snakeyaml/src/default/) | YAML parsing | Apache License 2.0 |
2 changes: 1 addition & 1 deletion doc/dialects/athena.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ You install the adapter script via the special SQL command `CREATE JAVA ADAPTER
```sql
CREATE OR REPLACE JAVA ADAPTER SCRIPT ADAPTER.JDBC_ADAPTER AS
%scriptclass com.exasol.adapter.jdbc.JdbcAdapter;
%jar /buckets/bucketfs1/jdbc/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/jdbc/virtualschema-jdbc-adapter-dist-1.18.1.jar;
%jar /buckets/bucketfs1/jdbc/AthenaJDBC42-<JDBC driver version>.jar;
/
```
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/bigquery.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Download the [Simba JDBC Driver for Google BigQuery](https://cloud.google.com/bi
--/
CREATE JAVA ADAPTER SCRIPT ADAPTER.JDBC_ADAPTER AS
%scriptclass com.exasol.adapter.RequestDispatcher;
%jar /buckets/bfsdefault/jars/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bfsdefault/jars/virtualschema-jdbc-adapter-dist-1.18.1.jar;
%jar /buckets/bfsdefault/jars/avro-1.8.2.jar;
%jar /buckets/bfsdefault/jars/gax-1.40.0.jar;
%jar /buckets/bfsdefault/jars/google-api-client-1.28.0.jar;
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/db2.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ CREATE or replace JAVA ADAPTER SCRIPT adapter.jdbc_adapter AS

// This will add the adapter jar to the classpath so that it can be used inside the adapter script
// Replace the names of the bucketfs and the bucket with the ones you used.
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar;

// DB2 Driver files
%jar /buckets/bucketfs1/bucket1/db2jcc4.jar;
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/exasol.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ After uploading the adapter jar, the adapter script can be created as follows:
CREATE SCHEMA adapter;
CREATE JAVA ADAPTER SCRIPT adapter.jdbc_adapter AS
%scriptclass com.exasol.adapter.jdbc.JdbcAdapter;
%jar /buckets/your-bucket-fs/your-bucket/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/your-bucket-fs/your-bucket/virtualschema-jdbc-adapter-dist-1.18.1.jar;
/
```

Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/hive.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ CREATE SCHEMA adapter;
CREATE JAVA ADAPTER SCRIPT jdbc_adapter AS
%scriptclass com.exasol.adapter.jdbc.JdbcAdapter;

%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar;

%jar /buckets/bucketfs1/bucket1/HiveJDBC41.jar;
/
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/impala.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ CREATE SCHEMA adapter;
CREATE JAVA ADAPTER SCRIPT jdbc_adapter AS
%scriptclass com.exasol.adapter.jdbc.JdbcAdapter;

%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar;

%jar /buckets/bucketfs1/bucket1/hive_metastore.jar;
%jar /buckets/bucketfs1/bucket1/hive_service.jar;
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/oracle.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ CREATE JAVA ADAPTER SCRIPT adapter.jdbc_oracle AS

// You need to replace `your-bucket-fs` and `your-bucket` to match the actual location
// of the adapter jar.
%jar /buckets/your-bucket-fs/your-bucket/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/your-bucket-fs/your-bucket/virtualschema-jdbc-adapter-dist-1.18.1.jar;

// Add the oracle jdbc driver to the classpath
%jar /buckets/bucketfs1/bucket1/ojdbc7-12.1.0.2.jar
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/postgresql.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ CREATE OR REPLACE JAVA ADAPTER SCRIPT adapter.jdbc_adapter

// This will add the adapter jar to the classpath so that it can be used inside the adapter script
// Replace the names of the bucketfs and the bucket with the ones you used.
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar;

// You have to add all files of the data source jdbc driver here (e.g. MySQL or Hive)
%jar /buckets/bucketfs1/bucket1/postgresql-42.0.0.jar;
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/redshift.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ You install the adapter script via the special SQL command `CREATE JAVA ADAPTER
```sql
CREATE OR REPLACE JAVA ADAPTER SCRIPT ADAPTER.JDBC_ADAPTER AS
%scriptclass com.exasol.adapter.jdbc.JdbcAdapter;
%jar /buckets/bucketfs1/jdbc/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/jdbc/virtualschema-jdbc-adapter-dist-1.18.1.jar;
%jar /buckets/bucketfs1/jdbc/RedshiftJDBC42-<JDBC driver version>.jar;
/
```
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/sql_server.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ CREATE OR REPLACE JAVA ADAPTER SCRIPT adapter.sql_server_jdbc_adapter

// This will add the adapter jar to the classpath so that it can be used inside the adapter script
// Replace the names of the bucketfs and the bucket with the ones you used.
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar;

// You have to add all files of the data source jdbc driver here
%jar /buckets/bucketfs1/bucket1/jtds.jar;
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/sybase.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ CREATE OR REPLACE JAVA ADAPTER SCRIPT adapter.jdbc_adapter
AS

%scriptclass com.exasol.adapter.jdbc.JdbcAdapter;
%jar /buckets/bucketfs1/virtualschema/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/virtualschema/virtualschema-jdbc-adapter-dist-1.18.1.jar;
%jar /buckets/bucketfs1/virtualschema/jtds-1.3.1.jar;
/
```
Expand Down
2 changes: 1 addition & 1 deletion doc/dialects/teradata.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ CREATE OR REPLACE JAVA ADAPTER SCRIPT adapter.jdbc_adapter

// This will add the adapter jar to the classpath so that it can be used inside the adapter script
// Replace the names of the bucketfs and the bucket with the ones you used.
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar;

// You have to add all files of the data source jdbc driver here (e.g. MySQL or Hive)
%jar /buckets/bucketfs1/bucket1/terajdbc4.jar;
Expand Down
8 changes: 4 additions & 4 deletions doc/user-guide/deploying_the_virtual_schema_adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ cd virtual-schemas/jdbc-adapter/
mvn clean -DskipTests package
```

The resulting fat JAR is stored in `virtualschema-jdbc-adapter-dist/target/virtualschema-jdbc-adapter-dist-1.18.0.jar`.
The resulting fat JAR is stored in `virtualschema-jdbc-adapter-dist/target/virtualschema-jdbc-adapter-dist-1.18.1.jar`.

## Uploading the Adapter JAR Archive

Expand All @@ -42,8 +42,8 @@ Following steps are required to upload a file to a bucket:
1. Now upload the file into this bucket, e.g. using curl (adapt the hostname, BucketFS port, bucket name and bucket write password).

```bash
curl -X PUT -T virtualschema-jdbc-adapter-dist/target/virtualschema-jdbc-adapter-dist-1.18.0.jar \
http://w:[email protected]:2580/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar
curl -X PUT -T virtualschema-jdbc-adapter-dist/target/virtualschema-jdbc-adapter-dist-1.18.1.jar \
http://w:[email protected]:2580/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar
```

Port number is always 2580.
Expand Down Expand Up @@ -77,7 +77,7 @@ CREATE JAVA ADAPTER SCRIPT adapter.jdbc_adapter AS

// This will add the adapter jar to the classpath so that it can be used inside the adapter script
// Replace the names of the bucketfs and the bucket with the ones you used.
%jar /buckets/your-bucket-fs/your-bucket/virtualschema-jdbc-adapter-dist-1.18.0.jar;
%jar /buckets/your-bucket-fs/your-bucket/virtualschema-jdbc-adapter-dist-1.18.1.jar;

// You have to add all files of the data source jdbc driver here (e.g. Hive JDBC driver files)
%jar /buckets/your-bucket-fs/your-bucket/name-of-data-source-jdbc-driver.jar;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ general:
debugAddress: '192.168.0.12:3000' # Address which will be defined as DEBUG_ADDRESS in the virtual schemas
bucketFsUrl: http://exasol-host:2580/bucket1
bucketFsPassword: bucket1
jdbcAdapterPath: /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar
jdbcAdapterPath: /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar

exasol:
runIntegrationTests: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ general:
debugAddress: '192.168.0.12:3000' # Address which will be defined as DEBUG_ADDRESS in the virtual schemas
bucketFsUrl: http://exasol-host:2580/bucket1
bucketFsPassword: bucket1
jdbcAdapterPath: /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.0.jar
jdbcAdapterPath: /buckets/bucketfs1/bucket1/virtualschema-jdbc-adapter-dist-1.18.1.jar

exasol:
runIntegrationTests: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ general:
debug: true
debugAddress: ''
bucketFsUrl: http://127.0.0.1:6594/default
jdbcAdapterPath: /buckets/bfsdefault/default/virtualschema-jdbc-adapter-dist-1.18.0.jar
jdbcAdapterPath: /buckets/bfsdefault/default/virtualschema-jdbc-adapter-dist-1.18.1.jar
additionalJDBCDriverDir: /vagrant/drivers/

exasol:
Expand Down
9 changes: 9 additions & 0 deletions jdbc-adapter/launch/virtual-schemas version.sh unify.launch
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<launchConfiguration type="org.eclipse.ui.externaltools.ProgramLaunchConfigurationType">
<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
<listEntry value="org.eclipse.ui.externaltools.launchGroup"/>
</listAttribute>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_LOCATION" value="${workspace_loc:/virtual-schemas/jdbc-adapter/tools/version.sh}"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_TOOL_ARGUMENTS" value="unify"/>
<stringAttribute key="org.eclipse.ui.externaltools.ATTR_WORKING_DIRECTORY" value="${workspace_loc:/virtual-schemas}"/>
</launchConfiguration>
2 changes: 1 addition & 1 deletion jdbc-adapter/local/integration-test-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ general:
debugAddress: '10.44.1.228:3000' # Address which will be defined as DEBUG_ADDRESS in the virtual schemas
bucketFsUrl: http://localhost:2580/jars
bucketFsPassword: public
jdbcAdapterPath: /buckets/bfsdefault/jars/virtualschema-jdbc-adapter-dist-1.18.0.jar
jdbcAdapterPath: /buckets/bfsdefault/jars/virtualschema-jdbc-adapter-dist-1.18.1.jar

exasol:
runIntegrationTests: true
Expand Down
10 changes: 2 additions & 8 deletions jdbc-adapter/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@
<module>virtualschema-jdbc-adapter-dist</module>
</modules>
<properties>
<product.version>1.18.0</product.version>
<product.version>1.18.1</product.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<junit.version>5.4.2</junit.version>
<junit.platform.version>1.4.2</junit.platform.version>
<maven.surefire.version>2.22.1</maven.surefire.version>
<vscommon.version>6.0.0</vscommon.version>
<vscommon.version>6.0.1</vscommon.version>
</properties>
<distributionManagement>
<repository>
Expand Down Expand Up @@ -87,12 +87,6 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.itsallcode</groupId>
<artifactId>junit5-system-extensions</artifactId>
<version>1.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.sql.Connection;
import java.sql.SQLException;
import java.util.*;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

Expand All @@ -19,13 +20,14 @@
* Abstract implementation of a dialect. We recommend that every dialect should extend this abstract class.
*/
public abstract class AbstractSqlDialect implements SqlDialect {
private static final Pattern BOOLEAN_PROPERTY_VALUE_PATTERN = Pattern.compile("^TRUE$|^FALSE$",
Pattern.CASE_INSENSITIVE);
protected Set<ScalarFunction> omitParenthesesMap = EnumSet.noneOf(ScalarFunction.class);
protected RemoteMetadataReader remoteMetadataReader;
protected AdapterProperties properties;
protected final Connection connection;
protected QueryRewriter queryRewriter;
private static final Pattern BOOLEAN_PROPERTY_VALUE_PATTERN = Pattern.compile("^TRUE$|^FALSE$",
Pattern.CASE_INSENSITIVE);
private static final Logger LOGGER = Logger.getLogger(AbstractSqlDialect.class.getName());

/**
* Create a new instance of an {@link AbstractSqlDialect}
Expand Down Expand Up @@ -196,21 +198,33 @@ protected void validateBooleanProperty(final String property) throws PropertyVal
}
}

private void validateDebugOutputAddress() throws PropertyValidationException {
private void validateDebugOutputAddress() {
if (this.properties.containsKey(DEBUG_ADDRESS_PROPERTY)) {
final String debugAddress = this.properties.getDebugAddress();
if (!debugAddress.isEmpty()) {
final String error = "You specified an invalid hostname and port where a log receiver (e.g. `netcat`) "
+ "is listening for incoming connections. The value of the property " + DEBUG_ADDRESS_PROPERTY
+ "must adhere to the following format: <host>:<port>, where host is a host name or IP address.";
if (debugAddress.split(":").length != 2) {
throw new PropertyValidationException(error);
}
try {
Integer.parseInt(debugAddress.split(":")[1]);
} catch (final NumberFormatException ex) {
throw new PropertyValidationException(error);
validateDebugPortNumber(debugAddress);
}
}
}

// Note that this method intentionally does not throw a validation exception but rather creates log warnings. This
// allows dropping a schema even if the debug output port is misconfigured. Logging falls back to local logging in
// this case.
private void validateDebugPortNumber(final String debugAddress) {
final int colonLocation = debugAddress.lastIndexOf(':');
if (colonLocation > 0) {
final String portAsString = debugAddress.substring(colonLocation + 1);
try {
final int port = Integer.parseInt(portAsString);
if ((port < 1) || (port > 65535)) {
LOGGER.warning(() -> "Debug output port " + port + " is out of range. Port specified in property "
+ DEBUG_ADDRESS_PROPERTY
+ "must have following format: <host>[:<port>], and be between 1 and 65535.");
}
} catch (final NumberFormatException ex) {
LOGGER.warning(() -> "Illegal debug output port \"" + portAsString + "\". Property "
+ DEBUG_ADDRESS_PROPERTY
+ "must have following format: <host>[:<port>], where port is a number between 1 and 65535.");
}
}
}
Expand Down Expand Up @@ -282,4 +296,4 @@ protected void checkImportPropertyConsistency(final String importFromProperty, f
List<String> getIgnoredErrors() {
return this.properties.getIgnoredErrors().stream().map(String::toUpperCase).collect(Collectors.toList());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,31 @@
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.util.*;
import java.util.logging.Logger;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

import com.exasol.adapter.AdapterProperties;
import com.exasol.logging.CapturingLogHandler;

class AbstractSqlDialectTest {
private Map<String, String> rawProperties;
private final CapturingLogHandler capturingLogHandler = new CapturingLogHandler();

@BeforeEach
void beforeEach() {
Logger.getLogger("com.exasol").addHandler(this.capturingLogHandler);
this.capturingLogHandler.reset();
this.rawProperties = new HashMap<>();
}

@AfterEach
void afterEach() {
Logger.getLogger("com.exasol").removeHandler(this.capturingLogHandler);
}

@Test
void testNoCredentials() {
this.rawProperties.put(SQL_DIALECT_PROPERTY, "GENERIC");
Expand Down Expand Up @@ -114,36 +123,29 @@ void testInvalidDialect() {
}

@Test
void testInvalidDebugAddress1() {
void testValidatePropertiesWithWherePortIsString() throws PropertyValidationException {
this.rawProperties.put(DEBUG_ADDRESS_PROPERTY, "host:port_should_be_a_number");
assertWarningIssued("Illegal debug output port");
}

private void assertWarningIssued(final String expectedMessagePart) throws PropertyValidationException {
getMinimumMandatory();
this.rawProperties.put(DEBUG_ADDRESS_PROPERTY, "bla");
final AdapterProperties adapterProperties = new AdapterProperties(this.rawProperties);
final SqlDialect sqlDialect = new DummySqlDialect(null, adapterProperties);
final PropertyValidationException exception = assertThrows(PropertyValidationException.class,
sqlDialect::validateProperties);
assertThat(exception.getMessage(), containsString("You specified an invalid hostname and port"));
sqlDialect.validateProperties();
assertThat(this.capturingLogHandler.getCapturedData(), containsString(expectedMessagePart));
}

@Test
void testInvalidDebugAddress2() {
getMinimumMandatory();
this.rawProperties.put(DEBUG_ADDRESS_PROPERTY, "bla:no-number");
final AdapterProperties adapterProperties = new AdapterProperties(this.rawProperties);
final SqlDialect sqlDialect = new DummySqlDialect(null, adapterProperties);
final PropertyValidationException exception = assertThrows(PropertyValidationException.class,
sqlDialect::validateProperties);
assertThat(exception.getMessage(), containsString("You specified an invalid hostname and port"));
void testValidatePropertiesWithWherePortTooLow() throws PropertyValidationException {
this.rawProperties.put(DEBUG_ADDRESS_PROPERTY, "host:0");
assertWarningIssued("Debug output port 0 is out of range");
}

@Test
void testInvalidDebugAddress3() {
getMinimumMandatory();
this.rawProperties.put(DEBUG_ADDRESS_PROPERTY, "bla:123:456");
final AdapterProperties adapterProperties = new AdapterProperties(this.rawProperties);
final SqlDialect sqlDialect = new DummySqlDialect(null, adapterProperties);
final PropertyValidationException exception = assertThrows(PropertyValidationException.class,
sqlDialect::validateProperties);
assertThat(exception.getMessage(), containsString("You specified an invalid hostname and port"));
void testValidatePropertiesWithWherePortTooHigh() throws PropertyValidationException {
this.rawProperties.put(DEBUG_ADDRESS_PROPERTY, "host:65536");
assertWarningIssued("Debug output port 65536 is out of range");
}

@Test
Expand Down Expand Up @@ -278,4 +280,4 @@ void testGetStringLiteralWithNull() {
final SqlDialect sqlDialect = new DummySqlDialect(null, AdapterProperties.emptyProperties());
assertThat(sqlDialect.getStringLiteral(null), equalTo("NULL"));
}
}
}
Loading

0 comments on commit c71a928

Please sign in to comment.