This commit is contained in:
Andrea Cavalli 2021-09-27 20:22:57 +02:00
parent a2249c4fcf
commit 688076ff00
15 changed files with 84 additions and 21 deletions

View File

@ -2,7 +2,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>it.tdlight</groupId> <groupId>it.tdlight</groupId>
<artifactId>example-java</artifactId> <artifactId>example-java</artifactId>
<version>1.0.0-SNAPSHOT</version> <version>1.0.0.0-SNAPSHOT</version>
<name>TDLight Java Example</name> <name>TDLight Java Example</name>
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
@ -25,7 +25,7 @@
<dependency> <dependency>
<groupId>it.tdlight</groupId> <groupId>it.tdlight</groupId>
<artifactId>tdlight-java</artifactId> <artifactId>tdlight-java</artifactId>
<version>2.7.8.1</version> <version>2.7.8.2</version>
</dependency> </dependency>
<!-- TDLight natives --> <!-- TDLight natives -->

View File

@ -67,7 +67,7 @@ public final class Example {
SimpleTelegramClient client = new SimpleTelegramClient(settings); SimpleTelegramClient client = new SimpleTelegramClient(settings);
// Configure the authentication info // Configure the authentication info
AuthenticationData authenticationData = AuthenticationData.bot("1458657665:AAFotSQj1pHcRrH1C0DmDX6VpwSanJgb2-g"); AuthenticationData authenticationData = AuthenticationData.bot("124:1999");
// Add an example update handler that prints when the bot is started // Add an example update handler that prints when the bot is started
client.addUpdateHandler(UpdateAuthorizationState.class, update -> printStatus(update.authorizationState)); client.addUpdateHandler(UpdateAuthorizationState.class, update -> printStatus(update.authorizationState));

View File

@ -4,7 +4,7 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://logging.apache.org/log4j/2.0/config xsi:schemaLocation="http://logging.apache.org/log4j/2.0/config
https://raw.githubusercontent.com/apache/logging-log4j2/log4j-2.14.1/log4j-core/src/main/resources/Log4j-config.xsd" https://raw.githubusercontent.com/apache/logging-log4j2/log4j-2.14.1/log4j-core/src/main/resources/Log4j-config.xsd"
status="INFO"> status="ALL">
<Appenders> <Appenders>
<Console name="Console" target="SYSTEM_OUT"> <Console name="Console" target="SYSTEM_OUT">
<PatternLayout <PatternLayout
@ -13,7 +13,7 @@
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="INFO"> <Root level="ALL">
<AppenderRef ref="Console"/> <AppenderRef ref="Console"/>
</Root> </Root>
</Loggers> </Loggers>

View File

@ -3,6 +3,7 @@ package it.tdlight.client;
import it.tdlight.common.TelegramClient; import it.tdlight.common.TelegramClient;
import it.tdlight.jni.TdApi; import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateReady; import it.tdlight.jni.TdApi.AuthorizationStateReady;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.GetMe; import it.tdlight.jni.TdApi.GetMe;
import it.tdlight.jni.TdApi.UpdateAuthorizationState; import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import it.tdlight.jni.TdApi.User; import it.tdlight.jni.TdApi.User;
@ -25,7 +26,12 @@ final class AuthorizationStateReadyGetMe implements GenericUpdateHandler<UpdateA
@Override @Override
public void onUpdate(UpdateAuthorizationState update) { public void onUpdate(UpdateAuthorizationState update) {
if (update.authorizationState.getConstructor() == AuthorizationStateReady.CONSTRUCTOR) { if (update.authorizationState.getConstructor() == AuthorizationStateReady.CONSTRUCTOR) {
client.send(new GetMe(), me -> this.me.set((User) me), error -> logger.warn("Failed to execute TdApi.GetMe()")); client.send(new GetMe(), me -> {
if (me.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) me);
}
this.me.set((User) me);
}, error -> logger.warn("Failed to execute TdApi.GetMe()"));
} }
} }
} }

View File

@ -5,7 +5,9 @@ package it.tdlight.client;
import it.tdlight.common.utils.ScannerUtils; import it.tdlight.common.utils.ScannerUtils;
import it.tdlight.jni.TdApi; import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateWaitEncryptionKey; import it.tdlight.jni.TdApi.AuthorizationStateWaitEncryptionKey;
import it.tdlight.jni.TdApi.AuthorizationStateWaitPhoneNumber;
import it.tdlight.jni.TdApi.CheckDatabaseEncryptionKey; import it.tdlight.jni.TdApi.CheckDatabaseEncryptionKey;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.PhoneNumberAuthenticationSettings; import it.tdlight.jni.TdApi.PhoneNumberAuthenticationSettings;
import it.tdlight.jni.TdApi.SetAuthenticationPhoneNumber; import it.tdlight.jni.TdApi.SetAuthenticationPhoneNumber;
import it.tdlight.jni.TdApi.UpdateAuthorizationState; import it.tdlight.jni.TdApi.UpdateAuthorizationState;
@ -35,7 +37,11 @@ final class AuthorizationStateWaitAuthenticationDataHandler implements GenericUp
if (authenticationData.isBot()) { if (authenticationData.isBot()) {
String botToken = authenticationData.getBotToken(); String botToken = authenticationData.getBotToken();
TdApi.CheckAuthenticationBotToken response = new TdApi.CheckAuthenticationBotToken(botToken); TdApi.CheckAuthenticationBotToken response = new TdApi.CheckAuthenticationBotToken(botToken);
client.send(response, ok -> {}, ex -> { client.send(response, ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
}, ex -> {
logger.error("Failed to set TDLight phone number or bot token!", ex); logger.error("Failed to set TDLight phone number or bot token!", ex);
exceptionHandler.onException(ex); exceptionHandler.onException(ex);
}); });
@ -44,7 +50,11 @@ final class AuthorizationStateWaitAuthenticationDataHandler implements GenericUp
String phoneNumber = String.valueOf(authenticationData.getUserPhoneNumber()); String phoneNumber = String.valueOf(authenticationData.getUserPhoneNumber());
SetAuthenticationPhoneNumber response = new SetAuthenticationPhoneNumber(phoneNumber, phoneSettings); SetAuthenticationPhoneNumber response = new SetAuthenticationPhoneNumber(phoneNumber, phoneSettings);
client.send(response, ok -> {}, ex -> { client.send(response, ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
}, ex -> {
logger.error("Failed to set TDLight phone number!", ex); logger.error("Failed to set TDLight phone number!", ex);
exceptionHandler.onException(ex); exceptionHandler.onException(ex);
}); });

View File

@ -6,6 +6,7 @@ import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateWaitCode; import it.tdlight.jni.TdApi.AuthorizationStateWaitCode;
import it.tdlight.jni.TdApi.AuthorizationStateWaitOtherDeviceConfirmation; import it.tdlight.jni.TdApi.AuthorizationStateWaitOtherDeviceConfirmation;
import it.tdlight.jni.TdApi.CheckAuthenticationCode; import it.tdlight.jni.TdApi.CheckAuthenticationCode;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Function; import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.UpdateAuthorizationState; import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -39,7 +40,11 @@ final class AuthorizationStateWaitCodeHandler implements GenericUpdateHandler<Up
); );
String code = clientInteraction.onParameterRequest(InputParameter.ASK_CODE, parameterInfo); String code = clientInteraction.onParameterRequest(InputParameter.ASK_CODE, parameterInfo);
Function response = new CheckAuthenticationCode(code); Function response = new CheckAuthenticationCode(code);
client.send(response, ok -> {}, ex -> { client.send(response, ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
}, ex -> {
logger.error("Failed to check authentication code", ex); logger.error("Failed to check authentication code", ex);
exceptionHandler.onException(ex); exceptionHandler.onException(ex);
}); });

View File

@ -5,6 +5,7 @@ import it.tdlight.common.TelegramClient;
import it.tdlight.jni.TdApi.AuthorizationStateWaitEncryptionKey; import it.tdlight.jni.TdApi.AuthorizationStateWaitEncryptionKey;
import it.tdlight.jni.TdApi.AuthorizationStateWaitTdlibParameters; import it.tdlight.jni.TdApi.AuthorizationStateWaitTdlibParameters;
import it.tdlight.jni.TdApi.CheckDatabaseEncryptionKey; import it.tdlight.jni.TdApi.CheckDatabaseEncryptionKey;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.SetTdlibParameters; import it.tdlight.jni.TdApi.SetTdlibParameters;
import it.tdlight.jni.TdApi.TdlibParameters; import it.tdlight.jni.TdApi.TdlibParameters;
import it.tdlight.jni.TdApi.UpdateAuthorizationState; import it.tdlight.jni.TdApi.UpdateAuthorizationState;
@ -26,7 +27,11 @@ final class AuthorizationStateWaitEncryptionKeyHandler implements GenericUpdateH
@Override @Override
public void onUpdate(UpdateAuthorizationState update) { public void onUpdate(UpdateAuthorizationState update) {
if (update.authorizationState.getConstructor() == AuthorizationStateWaitEncryptionKey.CONSTRUCTOR) { if (update.authorizationState.getConstructor() == AuthorizationStateWaitEncryptionKey.CONSTRUCTOR) {
client.send(new CheckDatabaseEncryptionKey(), ok -> {}, ex -> { client.send(new CheckDatabaseEncryptionKey(), ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
}, ex -> {
logger.error("Failed to manage TDLight database encryption key!", ex); logger.error("Failed to manage TDLight database encryption key!", ex);
exceptionHandler.onException(ex); exceptionHandler.onException(ex);
}); });

View File

@ -6,6 +6,7 @@ import it.tdlight.jni.TdApi.AuthorizationStateWaitCode;
import it.tdlight.jni.TdApi.AuthorizationStateWaitPassword; import it.tdlight.jni.TdApi.AuthorizationStateWaitPassword;
import it.tdlight.jni.TdApi.CheckAuthenticationCode; import it.tdlight.jni.TdApi.CheckAuthenticationCode;
import it.tdlight.jni.TdApi.CheckAuthenticationPassword; import it.tdlight.jni.TdApi.CheckAuthenticationPassword;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Function; import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.UpdateAuthorizationState; import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -38,7 +39,11 @@ final class AuthorizationStateWaitPasswordHandler implements GenericUpdateHandle
); );
String password = clientInteraction.onParameterRequest(InputParameter.ASK_PASSWORD, parameterInfo); String password = clientInteraction.onParameterRequest(InputParameter.ASK_PASSWORD, parameterInfo);
Function response = new CheckAuthenticationPassword(password); Function response = new CheckAuthenticationPassword(password);
client.send(response, ok -> {}, ex -> { client.send(response, ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
}, ex -> {
logger.error("Failed to check authentication password", ex); logger.error("Failed to check authentication password", ex);
exceptionHandler.onException(ex); exceptionHandler.onException(ex);
}); });

View File

@ -4,6 +4,7 @@ import it.tdlight.common.ExceptionHandler;
import it.tdlight.common.TelegramClient; import it.tdlight.common.TelegramClient;
import it.tdlight.jni.TdApi; import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.AuthorizationStateWaitRegistration; import it.tdlight.jni.TdApi.AuthorizationStateWaitRegistration;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.RegisterUser; import it.tdlight.jni.TdApi.RegisterUser;
import it.tdlight.jni.TdApi.UpdateAuthorizationState; import it.tdlight.jni.TdApi.UpdateAuthorizationState;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -50,7 +51,11 @@ final class AuthorizationStateWaitRegistrationHandler implements GenericUpdateHa
return; return;
} }
RegisterUser response = new RegisterUser(firstName, lastName); RegisterUser response = new RegisterUser(firstName, lastName);
client.send(response, ok -> {}, ex -> { client.send(response, ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
}, ex -> {
logger.error("Failed to register user", ex); logger.error("Failed to register user", ex);
exceptionHandler.onException(ex); exceptionHandler.onException(ex);
}); });

View File

@ -3,6 +3,7 @@ package it.tdlight.client;
import it.tdlight.common.ExceptionHandler; import it.tdlight.common.ExceptionHandler;
import it.tdlight.common.TelegramClient; import it.tdlight.common.TelegramClient;
import it.tdlight.jni.TdApi.AuthorizationStateWaitTdlibParameters; import it.tdlight.jni.TdApi.AuthorizationStateWaitTdlibParameters;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.SetTdlibParameters; import it.tdlight.jni.TdApi.SetTdlibParameters;
import it.tdlight.jni.TdApi.TdlibParameters; import it.tdlight.jni.TdApi.TdlibParameters;
import it.tdlight.jni.TdApi.UpdateAuthorizationState; import it.tdlight.jni.TdApi.UpdateAuthorizationState;
@ -44,7 +45,11 @@ final class AuthorizationStateWaitTdlibParametersHandler implements GenericUpdat
params.applicationVersion = settings.getApplicationVersion(); params.applicationVersion = settings.getApplicationVersion();
params.enableStorageOptimizer = settings.isStorageOptimizerEnabled(); params.enableStorageOptimizer = settings.isStorageOptimizerEnabled();
params.ignoreFileNames = settings.isIgnoreFileNames(); params.ignoreFileNames = settings.isIgnoreFileNames();
client.send(new SetTdlibParameters(params), ok -> {}, ex -> { client.send(new SetTdlibParameters(params), ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
}, ex -> {
logger.error("Failed to set TDLight parameters!", ex); logger.error("Failed to set TDLight parameters!", ex);
exceptionHandler.onException(ex); exceptionHandler.onException(ex);
}); });

View File

@ -3,6 +3,7 @@ package it.tdlight.client;
import it.tdlight.common.TelegramClient; import it.tdlight.common.TelegramClient;
import it.tdlight.jni.TdApi; import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Chat; import it.tdlight.jni.TdApi.Chat;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Message; import it.tdlight.jni.TdApi.Message;
import it.tdlight.jni.TdApi.MessageText; import it.tdlight.jni.TdApi.MessageText;
import it.tdlight.jni.TdApi.UpdateNewMessage; import it.tdlight.jni.TdApi.UpdateNewMessage;
@ -71,7 +72,12 @@ final class CommandsHandler implements GenericUpdateHandler<UpdateNewMessage> {
for (CommandHandler handler : handlers) { for (CommandHandler handler : handlers) {
client.send(new TdApi.GetChat(message.chatId), client.send(new TdApi.GetChat(message.chatId),
response -> handler.onCommand((Chat) response, message.sender, arguments), response -> {
if (response.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) response);
}
handler.onCommand((Chat) response, message.sender, arguments);
},
error -> logger.warn("Error when handling the command {}", commandName, error) error -> logger.warn("Error when handling the command {}", commandName, error)
); );
} }

View File

@ -11,6 +11,7 @@ import it.tdlight.common.utils.CantLoadLibrary;
import it.tdlight.common.utils.LibraryVersion; import it.tdlight.common.utils.LibraryVersion;
import it.tdlight.jni.TdApi; import it.tdlight.jni.TdApi;
import it.tdlight.jni.TdApi.Chat; import it.tdlight.jni.TdApi.Chat;
import it.tdlight.jni.TdApi.Error;
import it.tdlight.jni.TdApi.Function; import it.tdlight.jni.TdApi.Function;
import it.tdlight.jni.TdApi.Message; import it.tdlight.jni.TdApi.Message;
import it.tdlight.jni.TdApi.MessageText; import it.tdlight.jni.TdApi.MessageText;
@ -217,14 +218,22 @@ public final class SimpleTelegramClient implements Authenticable {
* Send the close signal but don't wait * Send the close signal but don't wait
*/ */
public void sendClose() { public void sendClose() {
client.send(new TdApi.Close(), ok -> {}); client.send(new TdApi.Close(), ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
});
} }
/** /**
* Send the close signal and wait for exit * Send the close signal and wait for exit
*/ */
public void closeAndWait() throws InterruptedException { public void closeAndWait() throws InterruptedException {
client.send(new TdApi.Close(), ok -> {}); client.send(new TdApi.Close(), ok -> {
if (ok.getConstructor() == Error.CONSTRUCTOR) {
throw new TelegramError((Error) ok);
}
});
this.waitForExit(); this.waitForExit();
} }

View File

@ -16,9 +16,12 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.slf4j.Marker;
import org.slf4j.MarkerFactory;
public final class InternalClient implements ClientEventsHandler, TelegramClient { public final class InternalClient implements ClientEventsHandler, TelegramClient {
private static final Marker TG_MARKER = MarkerFactory.getMarker("TG");
private static final Logger logger = LoggerFactory.getLogger(TelegramClient.class); private static final Logger logger = LoggerFactory.getLogger(TelegramClient.class);
private final ConcurrentHashMap<Long, Handler> handlers = new ConcurrentHashMap<Long, Handler>(); private final ConcurrentHashMap<Long, Handler> handlers = new ConcurrentHashMap<Long, Handler>();
@ -77,11 +80,12 @@ public final class InternalClient implements ClientEventsHandler, TelegramClient
} }
private void handleClose() { private void handleClose() {
logger.trace(TG_MARKER, "Received close");
handlers.forEach((eventId, handler) -> { handlers.forEach((eventId, handler) -> {
handleResponse(eventId, new Error(500, "Instance closed"), handler); handleResponse(eventId, new Error(500, "Instance closed"), handler);
}); });
handlers.clear(); handlers.clear();
logger.info("Client closed {}", clientId); logger.info(TG_MARKER, "Client closed {}", clientId);
} }
/** /**
@ -95,7 +99,7 @@ public final class InternalClient implements ClientEventsHandler, TelegramClient
handleException(handler.getExceptionHandler(), cause); handleException(handler.getExceptionHandler(), cause);
} }
} else { } else {
logger.error("Unknown event id \"{}\", the event has been dropped! {}", eventId, event); logger.error(TG_MARKER, "Unknown event id \"{}\", the event has been dropped! {}", eventId, event);
} }
} }
@ -103,6 +107,7 @@ public final class InternalClient implements ClientEventsHandler, TelegramClient
* Handles a response or an update * Handles a response or an update
*/ */
private void handleEvent(long eventId, Object event) { private void handleEvent(long eventId, Object event) {
logger.trace(TG_MARKER, "Received response {}: {}", eventId, event);
if (updatesHandler != null || updateHandler == null) throw new IllegalStateException(); if (updatesHandler != null || updateHandler == null) throw new IllegalStateException();
Handler handler = eventId == 0 ? updateHandler : handlers.remove(eventId); Handler handler = eventId == 0 ? updateHandler : handlers.remove(eventId);
handleResponse(eventId, event, handler); handleResponse(eventId, event, handler);
@ -143,7 +148,7 @@ public final class InternalClient implements ClientEventsHandler, TelegramClient
if (clientId != null) throw new UnsupportedOperationException("Can't initialize the same client twice!"); if (clientId != null) throw new UnsupportedOperationException("Can't initialize the same client twice!");
clientId = NativeClientAccess.create(); clientId = NativeClientAccess.create();
clientManager.registerClient(clientId, this); clientManager.registerClient(clientId, this);
logger.info("Registered new client {}", clientId); logger.info(TG_MARKER, "Registered new client {}", clientId);
// Send a dummy request because @levlam is too lazy to fix race conditions in a better way // Send a dummy request because @levlam is too lazy to fix race conditions in a better way
this.send(new TdApi.GetAuthorizationState(), (result) -> {}, ex -> {}); this.send(new TdApi.GetAuthorizationState(), (result) -> {}, ex -> {});
@ -151,6 +156,7 @@ public final class InternalClient implements ClientEventsHandler, TelegramClient
@Override @Override
public void send(Function query, ResultHandler resultHandler, ExceptionHandler exceptionHandler) { public void send(Function query, ResultHandler resultHandler, ExceptionHandler exceptionHandler) {
logger.trace(TG_MARKER, "Trying to send {}", query);
if (isClosedAndMaybeThrow(query)) { if (isClosedAndMaybeThrow(query)) {
resultHandler.onResult(new TdApi.Ok()); resultHandler.onResult(new TdApi.Ok());
} }
@ -168,6 +174,7 @@ public final class InternalClient implements ClientEventsHandler, TelegramClient
@Override @Override
public Object execute(Function query) { public Object execute(Function query) {
logger.trace(TG_MARKER, "Trying to execute {}", query);
if (isClosedAndMaybeThrow(query)) { if (isClosedAndMaybeThrow(query)) {
return new TdApi.Ok(); return new TdApi.Ok();
} }

View File

@ -7,7 +7,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<revision>1.0.0-SNAPSHOT</revision> <revision>1.0.0.0-SNAPSHOT</revision>
<api-version>3.3.147</api-version> <api-version>3.3.147</api-version>
<natives-version>3.3.149</natives-version> <natives-version>3.3.149</natives-version>
</properties> </properties>

View File

@ -7,7 +7,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<properties> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<revision>1.0.0-SNAPSHOT</revision> <revision>1.0.0.0-SNAPSHOT</revision>
<api-version>3.3.147</api-version> <api-version>3.3.147</api-version>
<natives-version>3.3.149</natives-version> <natives-version>3.3.149</natives-version>
</properties> </properties>