Skip to content

Commit af6daee

Browse files
Merge pull request #404 from crypto-chassis/java
docs: update README.md
2 parents e82dae8 + 2962760 commit af6daee

File tree

19 files changed

+360
-129
lines changed

19 files changed

+360
-129
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,4 @@ docs/_build/
149149
.project
150150
.venv/
151151
target/
152+
obj/

README.md

+36-20
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
Breaking changes in v6: Greetings, Ladies and Gentlemen, we've introduced some simplifications and breaking changes to our build process. Compared to v5, it should be much easier. If you have any questions, feel free to ping us on Discord https://discord.gg/b5EKcp9s8T. Thank you.
2-
31
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
42
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
53
**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
@@ -8,7 +6,7 @@ Breaking changes in v6: Greetings, Ladies and Gentlemen, we've introduced some s
86
- [Branches](#branches)
97
- [Build](#build)
108
- [C++](#c)
11-
- [Python](#python)
9+
- [non-C++](#non-c)
1210
- [Constants](#constants)
1311
- [Examples](#examples)
1412
- [Documentations](#documentations)
@@ -53,7 +51,7 @@ Breaking changes in v6: Greetings, Ladies and Gentlemen, we've introduced some s
5351
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
5452
# ccapi
5553
* A header-only C++ library for streaming market data and executing trades directly from cryptocurrency exchanges (i.e. the connections are between your server and the exchange server without anything in-between).
56-
* Bindings for other languages such as Python are provided.
54+
* Bindings for other languages such as Python and Java are provided.
5755
* Code closely follows Bloomberg's API: https://www.bloomberg.com/professional/support/api-library/.
5856
* It is ultra fast thanks to very careful optimizations: move semantics, regex optimization, locality of reference, lock contention minimization, etc.
5957
* Supported exchanges:
@@ -102,26 +100,29 @@ Breaking changes in v6: Greetings, Ladies and Gentlemen, we've introduced some s
102100
* "string table overflow at offset \<a>". Try to add optimization flag `-O1` or `-O2`. See https://stackoverflow.com/questions/14125007/gcc-string-table-overflow-error-during-compilation.
103101
* On Windows, if you still encounter resource related issues, try to add optimization flag `-O3 -DNDEBUG`.
104102

105-
### Python
106-
* Require Python 3, SWIG, and CMake.
107-
* SWIG: On macOS, `brew install SWIG`. On Linux, `sudo apt-get install -y swig`. On Windows, http://www.swig.org/Doc4.0/Windows.html#Windows.
103+
### non-C++
104+
* Require SWIG and CMake.
105+
* SWIG: On macOS, `brew install SWIG`. On Linux, `sudo apt-get install -y swig`.
108106
* CMake: https://cmake.org/download/.
109107
* Run the following commands.
110108
```
111109
mkdir binding/build
112110
cd binding/build
113111
rm -rf * (if rebuild from scratch)
114-
cmake -DBUILD_PYTHON=ON .. (optional: -DBUILD_VERSION=<anything>)
112+
cmake -DBUILD_PYTHON=ON -DBUILD_VERSION=1.0.0 .. (Use -DBUILD_JAVA=ON if the target language is Java)
115113
cmake --build .
116-
cmake --install .
117114
```
118-
* If a virtual environment (managed by `venv` or `conda`) is active (i.e. the `activate` script has been evaluated), the package will be installed into the virtual environment rather than globally.
119-
* Currently not working on Windows.
115+
* Python: If a virtual environment (managed by `venv` or `conda`) is active (i.e. the `activate` script has been evaluated), the package will be installed into the virtual environment rather than globally.
120116
* Troubleshoot:
121-
* "CMake Error at python/CMakeLists.txt:... (message): Require Python 3". Try to create and activate a virtual environment (managed by `venv` or `conda`) with Python 3.
122-
* "Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the system variable OPENSSL_ROOT_DIR (missing: OPENSSL_INCLUDE_DIR)". Try `cmake -DOPENSSL_ROOT_DIR=...`. On macOS, you might be missing headers for OpenSSL, `brew install openssl` and `cmake -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl`. On Ubuntu, `sudo apt-get install libssl-dev`. On Windows, `vcpkg install openssl:x64-windows` and `cmake -DOPENSSL_ROOT_DIR=C:/vcpkg/installed/x64-windows-static`.
123-
* "Fatal Python error: Segmentation fault". If the macOS version is relatively new and the Python version is relatively old, please upgrade Python to a relatively new version.
124-
* "‘_PyObject_GC_UNTRACK’ was not declared in this scope". If you use Python >= 3.8, please use SWIG >= 4.0.
117+
* Python:
118+
* "CMake Error at python/CMakeLists.txt:... (message): Require Python 3". Try to create and activate a virtual environment (managed by `venv` or `conda`) with Python 3.
119+
* "Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the system variable OPENSSL_ROOT_DIR (missing: OPENSSL_INCLUDE_DIR)". Try `cmake -DOPENSSL_ROOT_DIR=...`. On macOS, you might be missing headers for OpenSSL, `brew install openssl` and `cmake -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl`. On Ubuntu, `sudo apt-get install libssl-dev`. On Windows, `vcpkg install openssl:x64-windows` and `cmake -DOPENSSL_ROOT_DIR=C:/vcpkg/installed/x64-windows-static`.
120+
* "Fatal Python error: Segmentation fault". If the macOS version is relatively new and the Python version is relatively old, please upgrade Python to a relatively new version.
121+
* "‘_PyObject_GC_UNTRACK’ was not declared in this scope". If you use Python >= 3.8, please use SWIG >= 4.0.
122+
* Java:
123+
* "Could NOT find JNI (missing: JAVA_INCLUDE_PATH JAVA_INCLUDE_PATH2 JAVA_AWT_INCLUDE_PATH)". Check that the environment variable `JAVA_HOME` is correct.
124+
* "../Main.java:1: error: package com.cryptochassis.ccapi does not exist". Check that `javac`'s classpath includes `binding/build/java/packaging/1.0.0/ccapi-1.0.0.jar`.
125+
* "Exception in thread "main" java.lang.UnsatisfiedLinkError: no ccapi_binding_java in java.library.path: ...". Check that `java`'s `java.library.path` property includes `binding/build/java/packaging/1.0.0`. See https://stackoverflow.com/questions/1403788/java-lang-unsatisfiedlinkerror-no-dll-in-java-library-path.
125126

126127
## Constants
127128
[`include/ccapi_cpp/ccapi_macro.h`](include/ccapi_cpp/ccapi_macro.h)
@@ -142,8 +143,23 @@ cmake --build . --target <example-name>
142143

143144
[Python](binding/python/example)
144145
* Python API is nearly identical to C++ API and covers nearly all the functionalities from C++ API.
145-
* Build and install the Python binding as shown [above](#python).
146-
* Run `python3 main.py`.
146+
* Build and install the Python binding as shown [above](#non-c).
147+
* Inside a concrete example directory (e.g. binding/python/example/market_data_simple_subscription), run
148+
```
149+
python3 main.py
150+
```
151+
152+
[Java](binding/java/example)
153+
* Java API is nearly identical to C++ API and covers nearly all the functionalities from C++ API.
154+
* Build and install the Java binding as shown [above](#non-c).
155+
* Inside a concrete example directory (e.g. binding/python/example/market_data_simple_subscription), run
156+
```
157+
mkdir build
158+
cd build
159+
rm -rf * (if rebuild from scratch)
160+
javac -cp ../../../../build/java/packaging/1.0.0/ccapi-1.0.0.jar -d . ../Main.java
161+
java -cp .:../../../../build/java/packaging/1.0.0/ccapi-1.0.0.jar -Djava.library.path=../../../../build/java/packaging/1.0.0 Main
162+
```
147163

148164
## Documentations
149165

@@ -220,15 +236,15 @@ Bye
220236
```
221237
* Request operation types: `GET_INSTRUMENT`, `GET_INSTRUMENTS`, `GET_RECENT_TRADES`, `GET_RECENT_AGG_TRADES`(only applicable to binance family: https://binance-docs.github.io/apidocs/spot/en/#compressed-aggregate-trades-list).
222238
* Request parameter names: `LIMIT`, `INSTRUMENT_TYPE`. Instead of these convenient names you can also choose to use arbitrary parameter names and they will be passed to the exchange's native API. See [this example](example/src/market_data_advanced_request/main.cpp).
223-
* Message's `time` represents the exchange's reported timestamp. Its `timeReceived` represents the library's receiving timestamp. `time` can be retrieved by `getTime` method and `timeReceived` can be retrieved by `getTimeReceived` method. (For Python, please use `getTimeUnix` and `getTimeReceivedUnix` methods or `getTimeISO` and `getTimeReceivedISO` methods).
239+
* Message's `time` represents the exchange's reported timestamp. Its `timeReceived` represents the library's receiving timestamp. `time` can be retrieved by `getTime` method and `timeReceived` can be retrieved by `getTimeReceived` method. (For non-C++, please use `getTimeUnix` and `getTimeReceivedUnix` methods or `getTimeISO` and `getTimeReceivedISO` methods).
224240

225241
**Objective 2:**
226242

227243
For a specific exchange and instrument, whenever the best bid's or ask's price or size changes, print the market depth snapshot at that moment.
228244

229245
**Code 2:**
230246

231-
[C++](example/src/market_data_simple_subscription/main.cpp) / [Python](binding/python/example/market_data_simple_subscription/main.py)
247+
[C++](example/src/market_data_simple_subscription/main.cpp) / [Python](binding/python/example/market_data_simple_subscription/main.py) / [Java](binding/java/example/market_data_simple_subscription/Main.java)
232248
```
233249
#include "ccapi_cpp/ccapi_session.h"
234250
namespace ccapi {
@@ -402,7 +418,7 @@ For a specific exchange and instrument, submit a simple limit order.
402418

403419
**Code 1:**
404420

405-
[C++](example/src/execution_management_simple_request/main.cpp) / [Python](binding/python/example/execution_management_simple_request/main.py)
421+
[C++](example/src/execution_management_simple_request/main.cpp) / [Python](binding/python/example/execution_management_simple_request/main.py) / [Java](binding/java/example/execution_management_simple_request/Main.java)
406422
```
407423
#include "ccapi_cpp/ccapi_session.h"
408424
namespace ccapi {

binding/CMakeLists.txt

+28-5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ project(${NAME} LANGUAGES CXX)
66

77
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}")
88

9+
if(POLICY CMP0122)
10+
cmake_policy(SET CMP0122 NEW)
11+
endif()
12+
913
# Apple: Don't modify install_name when touching RPATH.
1014
if(POLICY CMP0068)
1115
cmake_policy(SET CMP0068 NEW)
@@ -73,13 +77,14 @@ else()
7377
endif()
7478

7579
option(BUILD_PYTHON "Build Python Library" OFF)
76-
option(INSTALL_PYTHON "Install Python Library" OFF)
7780
message(STATUS "Build Python: ${BUILD_PYTHON}")
7881

7982
option(BUILD_JAVA "Build Java Library" OFF)
80-
option(INSTALL_JAVA "Install Java Library" OFF)
8183
message(STATUS "Build Java: ${BUILD_JAVA}")
8284

85+
option(BUILD_CSHARP "Build C# Library" OFF)
86+
message(STATUS "Build C#: ${BUILD_CSHARP}")
87+
8388
set(CMAKE_CXX_STANDARD 17)
8489
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
8590
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
@@ -145,7 +150,7 @@ link_libraries(OpenSSL::Crypto OpenSSL::SSL)
145150

146151
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
147152
set(SOURCE_LOGGER ${CCAPI_PROJECT_DIR}/binding/ccapi_logger.cpp)
148-
set(CMAKE_SWIG_FLAGS)
153+
# set(CMAKE_SWIG_FLAGS)
149154
find_package(SWIG REQUIRED)
150155
include(UseSWIG)
151156
if(BUILD_TEST)
@@ -216,12 +221,30 @@ add_compile_definitions(CCAPI_ENABLE_EXCHANGE_WHITEBIT)
216221
find_package(ZLIB REQUIRED)
217222
link_libraries(ZLIB::ZLIB)
218223

219-
set(SWIG_INTERFACE ${CCAPI_PROJECT_DIR}/binding/swig_interface.i)
220-
221224
if(BUILD_PYTHON)
225+
configure_file(
226+
swig_interface.i.in
227+
${CMAKE_BINARY_DIR}/python/swig_interface.i
228+
@ONLY)
229+
set(SWIG_INTERFACE ${CMAKE_BINARY_DIR}/python/swig_interface.i)
222230
add_subdirectory(python)
223231
endif()
224232

225233
if(BUILD_JAVA)
234+
configure_file(
235+
swig_interface.i.in
236+
${CMAKE_BINARY_DIR}/java/swig_interface.i
237+
@ONLY)
238+
set(SWIG_INTERFACE ${CMAKE_BINARY_DIR}/java/swig_interface.i)
226239
add_subdirectory(java)
227240
endif()
241+
242+
if(BUILD_CSHARP)
243+
file(READ csharp/swig_interface_ccapi_language_specific.i SWIG_INTERFACE_CCAPI_LANGUAGE_SPECIFIC)
244+
configure_file(
245+
swig_interface.i.in
246+
${CMAKE_BINARY_DIR}/csharp/swig_interface.i
247+
@ONLY)
248+
set(SWIG_INTERFACE ${CMAKE_BINARY_DIR}/csharp/swig_interface.i)
249+
add_subdirectory(csharp)
250+
endif()

binding/csharp/CMakeLists.txt

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
set(NAME binding_csharp)
2+
project(${NAME})
3+
set(SWIG_TARGET_NAME ccapi_${NAME})
4+
5+
# Find dotnet cli
6+
find_program(DOTNET_EXECUTABLE NAMES dotnet REQUIRED)
7+
if(NOT DOTNET_EXECUTABLE)
8+
message(FATAL_ERROR "Check for dotnet Program: not found")
9+
else()
10+
message(STATUS "Found dotnet Program: ${DOTNET_EXECUTABLE}")
11+
endif()
12+
13+
execute_process(
14+
COMMAND ${DOTNET_EXECUTABLE} --version
15+
OUTPUT_VARIABLE DOTNET_EXECUTABLE_VERSION
16+
OUTPUT_STRIP_TRAILING_WHITESPACE
17+
)
18+
message(STATUS "Dotnet version: ${DOTNET_EXECUTABLE_VERSION}")
19+
20+
# set(CSHARP_DOMAIN_NAME "cryptochassis")
21+
# set(CSHARP_DOMAIN_EXTENSION "com")
22+
23+
# set(CSHARP_GROUP "${CSHARP_DOMAIN_EXTENSION}.${CSHARP_DOMAIN_NAME}")
24+
# set(CSHARP_ARTIFACT "ccapi")
25+
26+
set(CSHARP_NAMESPACE "ccapi")
27+
28+
set_property(SOURCE ${SWIG_INTERFACE} PROPERTY CPLUSPLUS ON)
29+
set_property(SOURCE ${SWIG_INTERFACE} PROPERTY COMPILE_OPTIONS "-namespace;${CSHARP_NAMESPACE};-dllimport;${SWIG_TARGET_NAME}.so")
30+
# set_property(SOURCE ${SWIG_INTERFACE} PROPERTY COMPILE_OPTIONS "-package;${CSHARP_PACKAGE};-doxygen")
31+
32+
swig_add_library(${SWIG_TARGET_NAME}
33+
LANGUAGE csharp
34+
OUTPUT_DIR ${CMAKE_BINARY_DIR}/csharp/${SWIG_TARGET_NAME}
35+
SOURCES ${SWIG_INTERFACE} ${SOURCE_LOGGER})
36+
37+
if(NOT CCAPI_LEGACY_USE_WEBSOCKETPP)
38+
add_dependencies(${SWIG_TARGET_NAME} boost rapidjson hffix)
39+
endif()
40+
# set_property(TARGET ${SWIG_TARGET_NAME} PROPERTY SWIG_USE_TARGET_INCLUDE_DIRECTORIES ON)
41+
# target_include_directories(${SWIG_TARGET_NAME}
42+
# PRIVATE
43+
# ${JNI_INCLUDE_DIRS}
44+
# )
45+
46+
47+
48+
set(PACKAGING_DIR packaging)
49+
set(PACKAGING_DIR_FULL ${CMAKE_CURRENT_BINARY_DIR}/${PACKAGING_DIR}/${BUILD_VERSION})
50+
file(MAKE_DIRECTORY ${PACKAGING_DIR_FULL})
51+
52+
set(SRC_DIR_FULL ${CMAKE_CURRENT_BINARY_DIR}/src)
53+
file(MAKE_DIRECTORY ${SRC_DIR_FULL})
54+
55+
configure_file(
56+
ccapi.csproj.in
57+
${SRC_DIR_FULL}/ccapi.csproj
58+
@ONLY)
59+
set(CSHARP_PACKAGING_TARGET_NAME csharp_${PACKAGING_DIR})
60+
add_custom_target(${CSHARP_PACKAGING_TARGET_NAME} ALL
61+
COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/${SWIG_TARGET_NAME}/*.cs ${SRC_DIR_FULL}
62+
# COMMAND ${DOTNET_EXECUTABLE} build -c ${CMAKE_BUILD_TYPE} ccapi.csproj
63+
COMMAND ${DOTNET_EXECUTABLE} build -c ${CMAKE_BUILD_TYPE} -o ${PACKAGING_DIR_FULL} ccapi.csproj
64+
COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:${SWIG_TARGET_NAME}> ${PACKAGING_DIR_FULL}
65+
WORKING_DIRECTORY ${SRC_DIR_FULL}
66+
)
67+
add_dependencies(${CSHARP_PACKAGING_TARGET_NAME} ${SWIG_TARGET_NAME})
68+
69+
# COMMAND ${Java_JAVAC_EXECUTABLE} -d ${PACKAGING_DIR_FULL} ${CMAKE_CURRENT_BINARY_DIR}/${SWIG_TARGET_NAME}/*.java
70+
# COMMAND ${CMAKE_COMMAND} -E copy $<TARGET_FILE:${SWIG_TARGET_NAME}> ${PACKAGING_DIR_FULL}

binding/csharp/ccapi.csproj.in

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
</PropertyGroup>
7+
8+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
class MainProgram
2+
{
3+
class MyEventHandler : ccapi.EventHandler {
4+
public override bool ProcessEvent(ccapi.Event event_, ccapi.Session session) {
5+
System.Console.WriteLine(System.String.Format("Received an event:\n%s", event_.ToStringPretty(2, 2)));
6+
return true;
7+
}
8+
}
9+
static void Main(string[] args)
10+
{
11+
if(System.Environment.GetEnvironmentVariable("OKX_API_KEY") is null) {
12+
System.Console.Error.WriteLine("Please set environment variable OKX_API_KEY");
13+
return;
14+
}
15+
if(System.Environment.GetEnvironmentVariable("OKX_API_SECRET") is null) {
16+
System.Console.Error.WriteLine("Please set environment variable OKX_API_SECRET");
17+
return;
18+
}
19+
if(System.Environment.GetEnvironmentVariable("OKX_API_PASSPHRASE") is null) {
20+
System.Console.Error.WriteLine("Please set environment variable OKX_API_PASSPHRASE");
21+
return;
22+
}
23+
var eventHandler = new MyEventHandler();
24+
var option = new ccapi.SessionOptions();
25+
var config = new ccapi.SessionConfigs();
26+
var session = new ccapi.Session(option, config, eventHandler);
27+
var request = new ccapi.Request(Request.Operation.CREATE_ORDER, "okx", "BTC-USDT");
28+
var param = new ccapi.MapStringString();
29+
param.Add("SIDE","BUY");
30+
param.Add("QUANTITY","0.0005");
31+
param.Add("LIMIT_PRICE","20000");
32+
request.AppendParam(param);
33+
session.SendRequest(request);
34+
System.Threading.Thread.Sleep(10000);
35+
session.Stop();
36+
System.Console.WriteLine("Bye");
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<CcapiLibraryPath>to be provided in command line</CcapiLibraryPath>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<Reference Include="ccapi">
10+
<HintPath>$(CcapiLibraryPath)</HintPath>
11+
</Reference>
12+
</ItemGroup>
13+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
class MainProgram
2+
{
3+
class MyEventHandler : ccapi.EventHandler {
4+
public override bool ProcessEvent(ccapi.Event event_, ccapi.Session session) {
5+
if (event_.GetType_() == ccapi.Event.Type.SUBSCRIPTION_DATA) {
6+
foreach (var message in event_.GetMessageList()) {
7+
System.Console.WriteLine(System.String.Format("Best bid and ask at {0} are:", message.GetTimeISO()));
8+
foreach (var element in message.GetElementList()){
9+
var elementNameValueMap = element.GetNameValueMap();
10+
foreach(var entry in elementNameValueMap)
11+
{
12+
var name = entry.Key;
13+
var value = entry.Value;
14+
System.Console.WriteLine(System.String.Format(" {0} = {1}", name, value));
15+
}
16+
}
17+
}
18+
}
19+
return true;
20+
}
21+
}
22+
static void Main(string[] args)
23+
{
24+
var eventHandler = new MyEventHandler();
25+
var option = new ccapi.SessionOptions();
26+
var config = new ccapi.SessionConfigs();
27+
var session = new ccapi.Session(option, config, eventHandler);
28+
var subscriptionList = new ccapi.SubscriptionList();
29+
subscriptionList.Add(new ccapi.Subscription("okx", "BTC-USDT", "MARKET_DEPTH"));
30+
session.Subscribe(subscriptionList);
31+
System.Threading.Thread.Sleep(10000);
32+
session.Stop();
33+
System.Console.WriteLine("Bye");
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<CcapiLibraryPath>to be provided in command line</CcapiLibraryPath>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<Reference Include="ccapi">
10+
<HintPath>$(CcapiLibraryPath)</HintPath>
11+
</Reference>
12+
</ItemGroup>
13+
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
%rename("%(camelcase)s", %$isfunction, %$ismember, %$ispublic) "";
2+
%rename("%(regex:/^(toString|getType|setType)$/\\u\\1_/)s") "";

0 commit comments

Comments
 (0)