Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
anis
anis-server
Commits
c70d2e28
Commit
c70d2e28
authored
May 27, 2019
by
François Agneray
Browse files
Merge branch '25-fusionner-searchdata-et-searchmeta' into 'develop'
#25
=> done Closes
#25
See merge request
!22
parents
6a248a90
56bad766
Pipeline
#1188
passed with stages
in 4 minutes and 18 seconds
Changes
7
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
src/Action/Search/Search
Meta
Action.php
→
src/Action/Search/SearchAction.php
View file @
c70d2e28
...
...
@@ -12,22 +12,25 @@ namespace App\Action\Search;
use
Psr\Log\LoggerInterface
;
use
Doctrine\ORM\EntityManagerInterface
;
use
Doctrine\DBAL\Query\QueryBuilder
;
use
Doctrine\DBAL\Query\Expression\CompositeExpression
;
use
Psr\Http\Message\ServerRequestInterface
as
Request
;
use
Psr\Http\Message\ResponseInterface
as
Response
;
use
App\Entity\Dataset
;
use
App\Entity\Attribute
;
use
App\Utils\ActionTrait
;
use
App\Utils\DBALConnectionFactory
;
use
App\utils\AnisQueryBuilderFactory
;
use
App\Search\SearchException
;
use
App\Search\Operator\IOperatorFactory
;
use
App\Entity\Dataset
;
use
App\Entity\Attribute
;
/**
* Get anis
meta
data search (GET)
* Get anis data search (GET)
*
* @author François Agneray <francois.agneray@lam.fr>
* @package App\Action\Search
*/
final
class
Search
Meta
Action
final
class
SearchAction
{
use
ActionTrait
;
...
...
@@ -53,11 +56,10 @@ final class SearchMetaAction
private
$dcf
;
/**
* Factory method allows to retrieve an anis query builder object used to construct the SQL request
*
* @var AnisQueryBuilderFactory
*
* @var IOperatorFactory
*/
private
$
aqbf
;
private
$
operatorFactory
;
/**
* The encryption key used by anis to encrypt and decrypt sensitive data like passwords
...
...
@@ -73,24 +75,24 @@ final class SearchMetaAction
* @param LoggerInterface $logger
* @param EntityManagerInterface $em
* @param DBALConnectionFactory $dcf
* @param
AnisQueryBuilderFactory $aqbf
* @param
IOperatorFactory $operatorFactory
*/
public
function
__construct
(
LoggerInterface
$logger
,
EntityManagerInterface
$em
,
DBALConnectionFactory
$dcf
,
AnisQueryBuilderFactory
$aqbf
,
IOperatorFactory
$operatorFactory
,
string
$encryptionKey
)
{
$this
->
logger
=
$logger
;
$this
->
em
=
$em
;
$this
->
dcf
=
$dcf
;
$this
->
aqbf
=
$aqbf
;
$this
->
operatorFactory
=
$operatorFactory
;
$this
->
encryptionKey
=
$encryptionKey
;
}
/**
* This action ret
rieves meta information about an anis request (ex: number of objects)
* This action ret
urns data from an anis request
*
* @param ServerRequestInterface $request This object contains the HTTP request
* @param ResponseInterface $response This object represents the HTTP response
...
...
@@ -107,6 +109,7 @@ final class SearchMetaAction
}
$dataset
=
$this
->
em
->
find
(
'App\Entity\Dataset'
,
$args
[
'dname'
]);
if
(
is_null
(
$dataset
))
{
return
$this
->
dispatchHttpError
(
$response
,
...
...
@@ -114,7 +117,7 @@ final class SearchMetaAction
'Dataset with id '
.
$args
[
'dname'
]
.
' is not found'
)
->
withStatus
(
404
);
}
$queryParams
=
$request
->
getQueryParams
();
if
(
!
array_key_exists
(
'a'
,
$queryParams
))
{
return
$this
->
dispatchHttpError
(
...
...
@@ -124,39 +127,111 @@ final class SearchMetaAction
)
->
withStatus
(
400
);
}
$listOfIds
=
explode
(
';'
,
$queryParams
[
'a'
]);
$attributesSelected
=
array
();
foreach
(
$listOfIds
as
$id
)
{
$attribute
=
$this
->
getAttribute
(
$dataset
,
(
int
)
$id
);
$attributesSelected
[]
=
array
(
'name'
=>
$attribute
->
getName
(),
'label'
=>
$attribute
->
getLabel
()
);
}
$database
=
$dataset
->
getProject
()
->
getDatabase
();
$decryptedPassword
=
$this
->
decryptData
(
$database
->
getPassword
());
$connection
=
$this
->
dcf
->
create
(
$database
,
$decryptedPassword
);
$queryBuilder
=
$connection
->
createQueryBuilder
();
$anisQueryBuilder
=
$this
->
aqbf
->
create
(
$queryBuilder
,
$dataset
);
$anisQueryBuilder
->
count
();
$queryBuilder
->
from
(
$dataset
->
getTableRef
());
if
(
array_key_exists
(
'c'
,
$queryParams
))
{
$
anisQueryBuilder
->
where
(
explode
(
';'
,
$queryParams
[
'c'
]));
$
this
->
where
(
$queryBuilder
,
$dataset
,
explode
(
';'
,
$queryParams
[
'c'
]));
}
$this
->
logger
->
info
(
'SQL: '
.
$anisQueryBuilder
->
getSQL
());
$result
=
$anisQueryBuilder
->
fetchAll
();
if
(
$args
[
'type'
]
===
'data'
)
{
$this
->
select
(
$queryBuilder
,
$dataset
,
explode
(
';'
,
$queryParams
[
'a'
]));
$meta
=
array
();
$meta
[
'dataset_selected'
]
=
$dataset
->
getLabel
();
$meta
[
'attributes_selected'
]
=
$attributesSelected
;
$meta
[
'total_items'
]
=
$result
[
0
][
'nb'
];
// $meta['url'] = $dataset->getName() . '?' . $request->getUri()->getQuery();
if
(
array_key_exists
(
'o'
,
$queryParams
))
{
$this
->
order
(
$queryBuilder
,
$dataset
,
explode
(
';'
,
$queryParams
[
'o'
]));
}
if
(
array_key_exists
(
'p'
,
$queryParams
))
{
$this
->
limit
(
$queryBuilder
,
$queryParams
[
'p'
]);
}
$result
=
$this
->
fetchAll
(
$queryBuilder
);
}
else
if
(
$args
[
'type'
]
===
'meta'
)
{
$queryBuilder
->
select
(
'COUNT(*) as nb'
);
$count
=
$this
->
fetchAll
(
$queryBuilder
);
$result
=
array
();
$result
[
'dataset_selected'
]
=
$dataset
->
getLabel
();
$result
[
'total_items'
]
=
$count
[
0
][
'nb'
];
}
else
{
throw
SearchException
::
typeOfSearchDoesNotExist
(
$args
[
'type'
]);
}
$this
->
logger
->
info
(
'SQL: '
.
$queryBuilder
->
getSQL
());
return
$response
->
withJson
(
$meta
);
return
$response
->
withJson
(
$result
);
}
private
function
select
(
QueryBuilder
$queryBuilder
,
Dataset
$dataset
,
array
$listOfIds
):
void
{
$columns
=
array
();
foreach
(
$listOfIds
as
$id
)
{
$attribute
=
$this
->
getAttribute
(
$dataset
,
(
int
)
$id
);
$columns
[]
=
$attribute
->
getTableName
()
.
'.'
.
$attribute
->
getName
()
.
' as '
.
$attribute
->
getLabel
();
}
$queryBuilder
->
select
(
$columns
);
}
private
function
where
(
QueryBuilder
$queryBuilder
,
Dataset
$dataset
,
array
$criteria
):
void
{
$expressions
=
array
();
foreach
(
$criteria
as
$criterion
)
{
$params
=
$this
->
getCriterionParams
(
$criterion
);
$attribute
=
$this
->
getAttribute
(
$dataset
,
(
int
)
$params
[
0
]);
$column
=
$attribute
->
getTableName
()
.
'.'
.
$attribute
->
getName
();
$columnType
=
$attribute
->
getType
();
if
(
array_key_exists
(
2
,
$params
))
{
$values
=
explode
(
'|'
,
$params
[
2
]);
}
else
{
$values
=
array
();
}
$operator
=
$this
->
operatorFactory
->
create
(
$params
[
1
],
$queryBuilder
->
expr
(),
$column
,
$columnType
,
$values
);
$expressions
[]
=
$operator
->
getExpression
();
}
$queryBuilder
->
where
(
new
CompositeExpression
(
CompositeExpression
::
TYPE_AND
,
$expressions
));
}
private
function
order
(
QueryBuilder
$queryBuilder
,
Dataset
$dataset
,
array
$orders
):
void
{
foreach
(
$orders
as
$order
)
{
$o
=
explode
(
':'
,
$order
);
$attribute
=
$this
->
getAttribute
(
$dataset
,
(
int
)
$o
[
0
]);
if
(
$o
[
1
]
===
'a'
)
{
$aord
=
'ASC'
;
}
else
if
(
$o
[
1
]
===
'd'
)
{
$aord
=
'DESC'
;
}
$queryBuilder
->
orderBy
(
$attribute
->
getTableName
()
.
'.'
.
$attribute
->
getName
(),
$aord
);
}
}
private
function
limit
(
QueryBuilder
$queryBuilder
,
string
$param
):
void
{
$p
=
explode
(
':'
,
$param
);
$limit
=
$p
[
0
];
$offset
=
(
$p
[
1
]
-
1
)
*
$limit
;
$queryBuilder
->
setFirstResult
(
$offset
)
->
setMaxResults
(
$limit
);
}
private
function
fetchAll
(
QueryBuilder
$queryBuilder
):
array
{
$stmt
=
$queryBuilder
->
execute
();
return
$stmt
->
fetchAll
();
}
private
function
getCriterionParams
(
string
$criterion
):
array
{
$params
=
explode
(
':'
,
$criterion
);
$numberOfParams
=
count
(
$params
);
if
(
$numberOfParams
<
2
)
{
throw
SearchException
::
numberOfCriterionParams
(
$numberOfParams
,
$criterion
);
}
return
$params
;
}
private
function
getAttribute
(
Dataset
$dataset
,
int
$id
):
Attribute
...
...
src/Action/Search/SearchDataAction.php
deleted
100644 → 0
View file @
6a248a90
<?php
declare
(
strict_types
=
1
);
/*
* This file is part of ANIS SERVER API.
*
* (c) François Agneray <francois.agneray@lam.fr>
* (c) Chrystel Moreau <chrystel.moreau@lam.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace
App\Action\Search
;
use
Psr\Log\LoggerInterface
;
use
Doctrine\ORM\EntityManagerInterface
;
use
Psr\Http\Message\ServerRequestInterface
as
Request
;
use
Psr\Http\Message\ResponseInterface
as
Response
;
use
App\Utils\ActionTrait
;
use
App\Utils\DBALConnectionFactory
;
use
App\utils\AnisQueryBuilderFactory
;
/**
* Get anis data search (GET)
*
* @author François Agneray <francois.agneray@lam.fr>
* @package App\Action\Search
*/
final
class
SearchDataAction
{
use
ActionTrait
;
/**
* The logger interface is the central access point to log anis information
*
* @var LoggerInterface
*/
private
$logger
;
/**
* The EntityManager is the central access point to Doctrine ORM functionality (metadata database)
*
* @var EntityManagerInterface
*/
private
$em
;
/**
* Factory method used to retrieve a PDO connection object to the business database
*
* @var DBALConnectionFactory
*/
private
$dcf
;
/**
* Factory method allows to retrieve an anis query builder object used to construct the SQL request
*
* @var AnisQueryBuilderFactory
*/
private
$aqbf
;
/**
* The encryption key used by anis to encrypt and decrypt sensitive data like passwords
* This key is provided by configuration (see the config file)
*
* @var string
*/
private
$encryptionKey
;
/**
* This class is creates before call __invoke to execute the action
*
* @param LoggerInterface $logger
* @param EntityManagerInterface $em
* @param DBALConnectionFactory $dcf
* @param AnisQueryBuilderFactory $aqbf
*/
public
function
__construct
(
LoggerInterface
$logger
,
EntityManagerInterface
$em
,
DBALConnectionFactory
$dcf
,
AnisQueryBuilderFactory
$aqbf
,
string
$encryptionKey
)
{
$this
->
logger
=
$logger
;
$this
->
em
=
$em
;
$this
->
dcf
=
$dcf
;
$this
->
aqbf
=
$aqbf
;
$this
->
encryptionKey
=
$encryptionKey
;
}
/**
* This action returns data from an anis request
*
* @param ServerRequestInterface $request This object contains the HTTP request
* @param ResponseInterface $response This object represents the HTTP response
* @param array $args This table contains information transmitted in the URL (see routes.php)
*
* @return ResponseInterface
*/
public
function
__invoke
(
Request
$request
,
Response
$response
,
array
$args
):
Response
{
$this
->
logger
->
info
(
'Search meta action dispatched'
);
if
(
$request
->
isOptions
())
{
return
$response
->
withHeader
(
'Access-Control-Allow-Methods'
,
'GET, OPTIONS'
);
}
$dataset
=
$this
->
em
->
find
(
'App\Entity\Dataset'
,
$args
[
'dname'
]);
if
(
is_null
(
$dataset
))
{
return
$this
->
dispatchHttpError
(
$response
,
'Invalid request'
,
'Dataset with id '
.
$args
[
'dname'
]
.
' is not found'
)
->
withStatus
(
404
);
}
$queryParams
=
$request
->
getQueryParams
();
if
(
!
array_key_exists
(
'a'
,
$queryParams
))
{
return
$this
->
dispatchHttpError
(
$response
,
'Invalid request'
,
'Param a is required for this request'
)
->
withStatus
(
400
);
}
$database
=
$dataset
->
getProject
()
->
getDatabase
();
$decryptedPassword
=
$this
->
decryptData
(
$database
->
getPassword
());
$connection
=
$this
->
dcf
->
create
(
$database
,
$decryptedPassword
);
$queryBuilder
=
$connection
->
createQueryBuilder
();
$anisQueryBuilder
=
$this
->
aqbf
->
create
(
$queryBuilder
,
$dataset
);
$anisQueryBuilder
->
select
(
explode
(
';'
,
$queryParams
[
'a'
]));
if
(
array_key_exists
(
'c'
,
$queryParams
))
{
$anisQueryBuilder
->
where
(
explode
(
';'
,
$queryParams
[
'c'
]));
}
if
(
array_key_exists
(
'o'
,
$queryParams
))
{
$anisQueryBuilder
->
order
(
explode
(
';'
,
$queryParams
[
'o'
]));
}
if
(
array_key_exists
(
'p'
,
$queryParams
))
{
$anisQueryBuilder
->
limit
(
$queryParams
[
'p'
]);
}
$this
->
logger
->
info
(
'SQL: '
.
$anisQueryBuilder
->
getSQL
());
$result
=
$anisQueryBuilder
->
fetchAll
();
return
$response
->
withJson
(
$result
);
}
}
src/Search/AnisQueryBuilder.php
deleted
100644 → 0
View file @
6a248a90
<?php
declare
(
strict_types
=
1
);
/*
* This file is part of ANIS SERVER API.
*
* (c) François Agneray <francois.agneray@lam.fr>
* (c) Chrystel Moreau <chrystel.moreau@lam.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace
App\Search
;
use
Doctrine\DBAL\Query\QueryBuilder
;
use
Doctrine\DBAL\Query\Expression\CompositeExpression
;
use
App\Search\Operator\IOperatorFactory
;
use
App\Entity\Dataset
;
use
App\Entity\Attribute
;
class
AnisQueryBuilder
{
private
$queryBuilder
;
private
$dataset
;
private
$operatorFactory
;
public
function
__construct
(
QueryBuilder
$queryBuilder
,
Dataset
$dataset
,
IOperatorFactory
$operatorFactory
)
{
$this
->
queryBuilder
=
$queryBuilder
->
from
(
$dataset
->
getTableRef
());
$this
->
dataset
=
$dataset
;
$this
->
operatorFactory
=
$operatorFactory
;
}
public
function
setOperatorFactory
(
string
$operatorFactory
):
void
{
$this
->
$operatorFactory
=
$operatorFactory
;
}
public
function
count
():
AnisQueryBuilder
{
$this
->
queryBuilder
->
select
(
'COUNT(*) as nb'
);
return
$this
;
}
public
function
select
(
array
$listOfIds
):
AnisQueryBuilder
{
$columns
=
array
();
foreach
(
$listOfIds
as
$id
)
{
$attribute
=
$this
->
getAttribute
((
int
)
$id
);
$columns
[]
=
$attribute
->
getTableName
()
.
'.'
.
$attribute
->
getName
()
.
' as '
.
$attribute
->
getLabel
();
}
$this
->
queryBuilder
->
select
(
$columns
);
return
$this
;
}
public
function
where
(
array
$criteria
):
AnisQueryBuilder
{
$expressions
=
array
();
foreach
(
$criteria
as
$criterion
)
{
$params
=
$this
->
getCriterionParams
(
$criterion
);
$attribute
=
$this
->
getAttribute
((
int
)
$params
[
0
]);
$column
=
$attribute
->
getTableName
()
.
'.'
.
$attribute
->
getName
();
$columnType
=
$attribute
->
getType
();
if
(
array_key_exists
(
2
,
$params
))
{
$values
=
explode
(
'|'
,
$params
[
2
]);
}
else
{
$values
=
array
();
}
$operator
=
$this
->
operatorFactory
->
create
(
$params
[
1
],
$this
->
queryBuilder
->
expr
(),
$column
,
$columnType
,
$values
);
$expressions
[]
=
$operator
->
getExpression
();
}
$this
->
queryBuilder
->
where
(
new
CompositeExpression
(
CompositeExpression
::
TYPE_AND
,
$expressions
));
return
$this
;
}
public
function
order
(
array
$orders
):
AnisQueryBuilder
{
foreach
(
$orders
as
$order
)
{
$o
=
explode
(
':'
,
$order
);
$attribute
=
$this
->
getAttribute
((
int
)
$o
[
0
]);
if
(
$o
[
1
]
===
'a'
)
{
$aord
=
'ASC'
;
}
else
if
(
$o
[
1
]
===
'd'
)
{
$aord
=
'DESC'
;
}
$this
->
queryBuilder
->
orderBy
(
$attribute
->
getTableName
()
.
'.'
.
$attribute
->
getName
(),
$aord
);
}
return
$this
;
}
public
function
limit
(
string
$param
):
AnisQueryBuilder
{
$p
=
explode
(
':'
,
$param
);
$limit
=
$p
[
0
];
$offset
=
(
$p
[
1
]
-
1
)
*
$limit
;
$this
->
queryBuilder
->
setFirstResult
(
$offset
)
->
setMaxResults
(
$limit
);
return
$this
;
}
public
function
getSQL
():
string
{
return
$this
->
queryBuilder
->
getSQL
();
}
public
function
fetchAll
():
array
{
$stmt
=
$this
->
queryBuilder
->
execute
();
return
$stmt
->
fetchAll
();
}
private
function
getCriterionParams
(
string
$criterion
):
array
{
$params
=
explode
(
':'
,
$criterion
);
$numberOfParams
=
count
(
$params
);
if
(
$numberOfParams
<
2
)
{
throw
SearchException
::
numberOfCriterionParams
(
$numberOfParams
,
$criterion
);
}
return
$params
;
}
private
function
getAttribute
(
int
$id
):
Attribute
{
$attributes
=
$this
->
dataset
->
getAttributes
();
foreach
(
$attributes
as
$attribute
)
{
if
(
$attribute
->
getId
()
===
$id
)
{
return
$attribute
;
}
}
throw
SearchException
::
attributeNotFound
(
$id
,
$this
->
dataset
->
getLabel
());
}
}
src/Search/SearchException.php
View file @
c70d2e28
...
...
@@ -39,4 +39,9 @@ class SearchException extends Exception
{
return
new
self
(
"List of ids is not correct"
);
}
public
static
function
typeOfSearchDoesNotExist
(
$param
)
{
return
new
self
(
"The type of search '"
.
$param
.
"' does not exists"
);
}
}
src/Utils/AnisQueryBuilderFactory.php
deleted
100644 → 0
View file @
6a248a90
<?php
declare
(
strict_types
=
1
);
/*
* This file is part of ANIS SERVER API.
*
* (c) François Agneray <francois.agneray@lam.fr>
* (c) Chrystel Moreau <chrystel.moreau@lam.fr>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace
App\Utils
;
use
Doctrine\DBAL\Query\QueryBuilder
;
use
App\Entity\Dataset
;
use
App\Search\Operator\IOperatorFactory
;
use
App\Search\AnisQueryBuilder
;
/**
* Factory used to create an anis query builder
*
* @author François Agneray <francois.agneray@lam.fr>
* @package App\Utils
*/
class
AnisQueryBuilderFactory
{
private
$operatorFactory
;
public
function
__construct
(
IOperatorFactory
$operatorFactory
)
{
$this
->
operatorFactory
=
$operatorFactory
;
}
public
function
create
(
QueryBuilder
$queryBuilder
,
Dataset
$dataset
):
AnisQueryBuilder
{
return
new
AnisQueryBuilder
(
$queryBuilder
,
$dataset
,
$this
->
operatorFactory
);
}
}
src/dependencies.php
View file @
c70d2e28
...
...
@@ -62,11 +62,6 @@ $container['dcf'] = function ($c) {
return
new
App\Utils\DBALConnectionFactory
();
};
// ANIS Query Builder Factory
$container
[
'aqbf'
]
=
function
(
$c
)
{
return
new
App\Utils\AnisQueryBuilderFactory
(
$c
->
get
(
'of'
));
};
// Middleware
$container
[
'App\Middleware\CorsMiddleware'
]
=
function
(
$c
)
{
return
new
App\Middleware\CorsMiddleware
();
...
...
@@ -226,12 +221,7 @@ $container['App\Action\Meta\UserAction'] = function ($c) {
};
// Search actions
$container
[
'App\Action\Search\SearchMetaAction'
]
=
function
(
$c
)
{
$settings
=
$c
->
get
(
'settings'
);
return
new
App\Action\Search\SearchMetaAction
(
$c
->
get
(
'logger'
),
$c
->
get
(
'em'
),
$c
->
get
(
'dcf'
),
$c
->
get
(
'aqbf'
),
$settings
[
'encryption_key'
]);
};
$container
[
'App\Action\Search\SearchDataAction'
]
=
function
(
$c
)
{
$container
[
'App\Action\Search\SearchAction'
]
=
function
(
$c
)
{
$settings
=
$c
->
get
(
'settings'
);
return
new
App\Action\Search\Search
Data
Action
(
$c
->
get
(
'logger'
),
$c
->
get
(
'em'
),
$c
->
get
(
'dcf'
),
$c
->
get
(
'
aqb
f'
),
$settings
[
'encryption_key'
]);
return
new
App\Action\Search\SearchAction
(
$c
->
get
(
'logger'
),
$c
->
get
(
'em'
),
$c
->
get
(
'dcf'
),
$c
->
get
(
'
o
f'
),
$settings
[
'encryption_key'
]);
};
src/routes.php
View file @
c70d2e28
...
...
@@ -48,5 +48,4 @@ $app->map(['OPTIONS', 'POST'], '/metadata/superuser', App\Action\Meta\SuperuserA
$app
->
map
([
'OPTIONS'
,
'GET'
],
'/metadata/user'
,
App\Action\Meta\UserListAction
::
class
);
$app
->
map
([
'OPTIONS'
,
'GET'
,
'PUT'
,
'DELETE'
],
'/metadata/user/{email}'
,
App\Action\Meta\UserAction
::
class
);
$app
->
map
([
'OPTIONS'
,
'GET'
],
'/search/meta/{dname}'
,
App\Action\Search\SearchMetaAction
::
class
);
$app
->
map
([
'OPTIONS'
,
'GET'
],
'/search/data/{dname}'
,
App\Action\Search\SearchDataAction
::
class
);
$app
->
map
([
'OPTIONS'
,
'GET'
],
'/search/{type}/{dname}'
,
App\Action\Search\SearchAction
::
class
);
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment