Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
C
corepost
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Commits
Open sidebar
nexlab
corepost
Commits
4ecf20e7
Commit
4ecf20e7
authored
Jun 27, 2012
by
Jacek Furmankiewicz
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of
https://github.com/kaosat-dev/corepost
parents
b2b41a31
4204008e
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
109 additions
and
61 deletions
+109
-61
convert.py
corepost/convert.py
+41
-14
enums.py
corepost/enums.py
+7
-1
routing.py
corepost/routing.py
+58
-37
utils.py
corepost/utils.py
+3
-9
No files found.
corepost/convert.py
View file @
4ecf20e7
...
@@ -7,12 +7,23 @@ for JSON/XML/YAML output
...
@@ -7,12 +7,23 @@ for JSON/XML/YAML output
'''
'''
import
collections
import
collections
import
logging
import
json
from
UserDict
import
DictMixin
from
UserDict
import
DictMixin
from
twisted.python
import
log
advanced_json
=
False
try
:
import
jsonpickle
advanced_json
=
True
except
ImportError
:
pass
primitives
=
(
int
,
long
,
float
,
bool
,
str
,
unicode
)
primitives
=
(
int
,
long
,
float
,
bool
,
str
,
unicode
)
def
convertForSerialization
(
obj
):
def
convertForSerialization
(
obj
):
"""Converts anything (clas,tuples,list) to the safe serializable equivalent"""
"""Converts anything (clas,tuples,list) to the safe serializable equivalent"""
try
:
if
type
(
obj
)
in
primitives
:
if
type
(
obj
)
in
primitives
:
# no conversion
# no conversion
return
obj
return
obj
...
@@ -29,6 +40,9 @@ def convertForSerialization(obj):
...
@@ -29,6 +40,9 @@ def convertForSerialization(obj):
else
:
else
:
# return as-is
# return as-is
return
obj
return
obj
except
AttributeError
as
ex
:
log
.
msg
(
ex
,
logLevel
=
logging
.
WARN
)
return
obj
def
convertClassToDict
(
clazz
):
def
convertClassToDict
(
clazz
):
"""Converts a class to a dictionary"""
"""Converts a class to a dictionary"""
...
@@ -49,6 +63,19 @@ def traverseDict(dictObject):
...
@@ -49,6 +63,19 @@ def traverseDict(dictObject):
return
newDict
return
newDict
def
convertToJson
(
obj
):
"""Converts to JSON, including Python classes that are not JSON serializable by default"""
if
advanced_json
:
try
:
return
jsonpickle
.
encode
(
obj
,
unpicklable
=
False
)
except
Exception
as
ex
:
raise
RuntimeError
(
str
(
ex
))
try
:
return
json
.
dumps
(
obj
)
except
Exception
as
ex
:
raise
RuntimeError
(
str
(
ex
))
def
generateXml
(
obj
):
def
generateXml
(
obj
):
"""Generates basic XML from an object that has already been converted for serialization"""
"""Generates basic XML from an object that has already been converted for serialization"""
if
isinstance
(
obj
,
dict
)
or
isinstance
(
obj
,
DictMixin
):
if
isinstance
(
obj
,
dict
)
or
isinstance
(
obj
,
DictMixin
):
...
...
corepost/enums.py
View file @
4ecf20e7
...
@@ -4,18 +4,24 @@ Common enums
...
@@ -4,18 +4,24 @@ Common enums
@author: jacekf
@author: jacekf
'''
'''
class
Http
:
class
Http
:
"""Enumerates HTTP methods"""
"""Enumerates HTTP methods"""
GET
=
"GET"
GET
=
"GET"
POST
=
"POST"
POST
=
"POST"
PUT
=
"PUT"
PUT
=
"PUT"
DELETE
=
"DELETE"
DELETE
=
"DELETE"
OPTIONS
=
"OPTIONS"
HEAD
=
"HEAD"
PATCH
=
"PATCH"
class
HttpHeader
:
class
HttpHeader
:
"""Enumerates common HTTP headers"""
"""Enumerates common HTTP headers"""
CONTENT_TYPE
=
"content-type"
CONTENT_TYPE
=
"content-type"
ACCEPT
=
"accept"
ACCEPT
=
"accept"
class
MediaType
:
class
MediaType
:
"""Enumerates media types"""
"""Enumerates media types"""
WILDCARD
=
"*/*"
WILDCARD
=
"*/*"
...
...
corepost/routing.py
View file @
4ecf20e7
...
@@ -7,23 +7,31 @@ Common routing classes, regardless of whether used in HTTP or multiprocess conte
...
@@ -7,23 +7,31 @@ Common routing classes, regardless of whether used in HTTP or multiprocess conte
from
collections
import
defaultdict
from
collections
import
defaultdict
from
corepost
import
Response
,
RESTException
from
corepost
import
Response
,
RESTException
from
corepost.enums
import
Http
,
HttpHeader
from
corepost.enums
import
Http
,
HttpHeader
from
corepost.utils
import
getMandatoryArgumentNames
,
convertToJson
,
safeDictUpdate
from
corepost.utils
import
getMandatoryArgumentNames
,
safeDictUpdate
from
corepost.convert
import
convertForSerialization
,
generateXml
from
corepost.convert
import
convertForSerialization
,
generateXml
,
convertToJson
from
corepost.filters
import
IRequestFilter
,
IResponseFilter
from
corepost.filters
import
IRequestFilter
,
IResponseFilter
from
enums
import
MediaType
from
enums
import
MediaType
from
twisted.internet
import
defer
from
twisted.internet
import
defer
from
twisted.web.http
import
parse_qs
from
twisted.web.http
import
parse_qs
from
twisted.python
import
log
from
twisted.python
import
log
import
re
,
copy
,
exceptions
,
json
,
yaml
,
logging
import
re
,
copy
,
exceptions
,
yaml
,
json
,
logging
from
xml.etree
import
ElementTree
from
xml.etree
import
ElementTree
import
uuid
advanced_json
=
False
try
:
import
jsonpickle
advanced_json
=
True
except
ImportError
:
pass
class
UrlRouter
:
class
UrlRouter
:
''' Common class for containing info related to routing a request to a function '''
''' Common class for containing info related to routing a request to a function '''
__urlMatcher
=
re
.
compile
(
r"<(int|float|):?([^/]+)>"
)
__urlMatcher
=
re
.
compile
(
r"<(int|float|
uuid|
):?([^/]+)>"
)
__urlRegexReplace
=
{
""
:
r"(?P<arg>([^/]+))"
,
"int"
:
r"(?P<arg>\d+)"
,
"float"
:
r"(?P<arg>\d+.?\d*)"
}
__urlRegexReplace
=
{
""
:
r"(?P<arg>([^/]+))"
,
"int"
:
r"(?P<arg>\d+)"
,
"float"
:
r"(?P<arg>\d+.?\d*)"
,
"uuid"
:
r"(?P<arg>[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})"
}
__typeConverters
=
{
"int"
:
int
,
"float"
:
float
}
__typeConverters
=
{
"int"
:
int
,
"float"
:
float
,
"uuid"
:
uuid
.
UUID
}
def
__init__
(
self
,
f
,
url
,
methods
,
accepts
,
produces
,
cache
):
def
__init__
(
self
,
f
,
url
,
methods
,
accepts
,
produces
,
cache
):
self
.
__f
=
f
self
.
__f
=
f
...
@@ -140,16 +148,16 @@ class RequestRouter:
...
@@ -140,16 +148,16 @@ class RequestRouter:
'''
'''
Constructor
Constructor
'''
'''
self
.
__urls
=
{
Http
.
GET
:
defaultdict
(
dict
),
Http
.
POST
:
defaultdict
(
dict
),
Http
.
PUT
:
defaultdict
(
dict
),
Http
.
DELETE
:
defaultdict
(
dict
)}
self
.
__urls
=
{
Http
.
GET
:
defaultdict
(
dict
),
Http
.
POST
:
defaultdict
(
dict
),
Http
.
PUT
:
defaultdict
(
dict
),
Http
.
DELETE
:
defaultdict
(
dict
)
,
Http
.
OPTIONS
:
defaultdict
(
dict
),
Http
.
PATCH
:
defaultdict
(
dict
),
Http
.
HEAD
:
defaultdict
(
dict
)
}
self
.
__cachedUrls
=
{
Http
.
GET
:
defaultdict
(
dict
),
Http
.
POST
:
defaultdict
(
dict
),
Http
.
PUT
:
defaultdict
(
dict
),
Http
.
DELETE
:
defaultdict
(
dict
)}
self
.
__cachedUrls
=
{
Http
.
GET
:
defaultdict
(
dict
),
Http
.
POST
:
defaultdict
(
dict
),
Http
.
PUT
:
defaultdict
(
dict
),
Http
.
DELETE
:
defaultdict
(
dict
)
,
Http
.
OPTIONS
:
defaultdict
(
dict
),
Http
.
PATCH
:
defaultdict
(
dict
),
Http
.
HEAD
:
defaultdict
(
dict
)
}
self
.
__urlRouterInstances
=
{}
self
.
__urlRouterInstances
=
{}
self
.
__schema
=
schema
self
.
__schema
=
schema
self
.
__urlsMehods
=
{}
self
.
__registerRouters
(
restServiceContainer
)
self
.
__registerRouters
(
restServiceContainer
)
self
.
__urlContainer
=
restServiceContainer
self
.
__urlContainer
=
restServiceContainer
self
.
__requestFilters
=
[]
self
.
__requestFilters
=
[]
self
.
__responseFilters
=
[]
self
.
__responseFilters
=
[]
#filters
if
filters
!=
None
:
if
filters
!=
None
:
for
webFilter
in
filters
:
for
webFilter
in
filters
:
valid
=
False
valid
=
False
...
@@ -167,7 +175,7 @@ class RequestRouter:
...
@@ -167,7 +175,7 @@ class RequestRouter:
def
path
(
self
):
def
path
(
self
):
return
self
.
__path
return
self
.
__path
def
__registerRouters
(
self
,
restServiceContainer
):
def
__registerRouters
(
self
,
restServiceContainer
):
"""Main method responsible for registering routers"""
"""Main method responsible for registering routers"""
from
types
import
FunctionType
from
types
import
FunctionType
...
@@ -179,24 +187,27 @@ class RequestRouter:
...
@@ -179,24 +187,27 @@ class RequestRouter:
func
=
service
.
__class__
.
__dict__
[
key
]
func
=
service
.
__class__
.
__dict__
[
key
]
# handle REST resources directly on the CorePost resource
# handle REST resources directly on the CorePost resource
if
type
(
func
)
==
FunctionType
and
hasattr
(
func
,
'corepostRequestRouter'
):
if
type
(
func
)
==
FunctionType
and
hasattr
(
func
,
'corepostRequestRouter'
):
# if specified, add class path to each function's path
# if specified, add class path to each function's path
rq
=
func
.
corepostRequestRouter
rq
=
func
.
corepostRequestRouter
#workaround for multiple passes of __registerRouters (for unit tests etc)
if
not
hasattr
(
rq
,
'urlAdapted'
):
rq
.
url
=
"
%
s
%
s"
%
(
rootPath
,
rq
.
url
)
rq
.
url
=
"
%
s
%
s"
%
(
rootPath
,
rq
.
url
)
# remove first and trailing '/' to standardize URLs
# remove first and trailing '/' to standardize URLs
start
=
1
if
rq
.
url
[
0
:
1
]
==
"/"
else
0
start
=
1
if
rq
.
url
[
0
:
1
]
==
"/"
else
0
end
=
-
1
if
rq
.
url
[
len
(
rq
.
url
)
-
1
]
==
'/'
else
len
(
rq
.
url
)
end
=
-
1
if
rq
.
url
[
len
(
rq
.
url
)
-
1
]
==
'/'
else
len
(
rq
.
url
)
rq
.
url
=
rq
.
url
[
start
:
end
]
rq
.
url
=
rq
.
url
[
start
:
end
]
setattr
(
rq
,
'urlAdapted'
,
True
)
# now that the full URL is set, compile the matcher for it
# now that the full URL is set, compile the matcher for it
rq
.
compileMatcherForFullUrl
()
rq
.
compileMatcherForFullUrl
()
for
method
in
rq
.
methods
:
for
method
in
rq
.
methods
:
for
accepts
in
rq
.
accepts
:
for
accepts
in
rq
.
accepts
:
urlRouterInstance
=
UrlRouterInstance
(
service
,
rq
)
urlRouterInstance
=
UrlRouterInstance
(
service
,
rq
)
self
.
__urls
[
method
][
rq
.
url
][
accepts
]
=
urlRouterInstance
self
.
__urls
[
method
][
rq
.
url
][
accepts
]
=
urlRouterInstance
self
.
__urlRouterInstances
[
func
]
=
urlRouterInstance
# needed so that we can lookup the urlRouterInstance for a specific function
self
.
__urlRouterInstances
[
func
]
=
urlRouterInstance
# needed so that we can lookup the urlRouterInstance for a specific function
if
self
.
__urlsMehods
.
get
(
rq
.
url
,
None
)
is
None
:
self
.
__urlsMehods
[
rq
.
url
]
=
[]
self
.
__urlsMehods
[
rq
.
url
]
.
append
(
method
)
def
getResponse
(
self
,
request
):
def
getResponse
(
self
,
request
):
"""Finds the appropriate instance and dispatches the request to the registered function. Returns the appropriate Response object"""
"""Finds the appropriate instance and dispatches the request to the registered function. Returns the appropriate Response object"""
...
@@ -236,11 +247,11 @@ class RequestRouter:
...
@@ -236,11 +247,11 @@ class RequestRouter:
# see if the path arguments match up against any function @route definition
# see if the path arguments match up against any function @route definition
args
=
instance
.
urlRouter
.
getArguments
(
path
)
args
=
instance
.
urlRouter
.
getArguments
(
path
)
if
args
!=
None
:
if
args
!=
None
:
if
instance
.
urlRouter
.
cache
:
if
instance
.
urlRouter
.
cache
:
self
.
__cachedUrls
[
request
.
method
][
path
][
contentType
]
=
CachedUrl
(
instance
,
args
)
self
.
__cachedUrls
[
request
.
method
][
path
][
contentType
]
=
CachedUrl
(
instance
,
args
)
urlRouterInstance
,
pathargs
=
instance
,
args
urlRouterInstance
,
pathargs
=
instance
,
args
break
break
#actual call
#actual call
if
urlRouterInstance
!=
None
and
pathargs
!=
None
:
if
urlRouterInstance
!=
None
and
pathargs
!=
None
:
allargs
=
copy
.
deepcopy
(
pathargs
)
allargs
=
copy
.
deepcopy
(
pathargs
)
...
@@ -286,6 +297,10 @@ class RequestRouter:
...
@@ -286,6 +297,10 @@ class RequestRouter:
log
.
err
(
ex
)
log
.
err
(
ex
)
response
=
self
.
__createErrorResponse
(
request
,
500
,
"Unexpected server error:
%
s
\n
%
s"
%
(
type
(
ex
),
ex
))
response
=
self
.
__createErrorResponse
(
request
,
500
,
"Unexpected server error:
%
s
\n
%
s"
%
(
type
(
ex
),
ex
))
#if a url is defined, but not the requested method
elif
not
request
.
method
in
self
.
__urlsMehods
.
get
(
path
,
[])
and
self
.
__urlsMehods
.
get
(
path
,
[])
!=
[]:
response
=
self
.
__createErrorResponse
(
request
,
501
,
""
)
else
:
else
:
log
.
msg
(
"URL
%
s not found"
%
path
,
logLevel
=
logging
.
WARN
)
log
.
msg
(
"URL
%
s not found"
%
path
,
logLevel
=
logging
.
WARN
)
response
=
self
.
__createErrorResponse
(
request
,
404
,
"URL '
%
s' not found
\n
"
%
request
.
path
)
response
=
self
.
__createErrorResponse
(
request
,
404
,
"URL '
%
s' not found
\n
"
%
request
.
path
)
...
@@ -305,9 +320,7 @@ class RequestRouter:
...
@@ -305,9 +320,7 @@ class RequestRouter:
Takes care of automatically rendering the response and converting it to appropriate format (text,XML,JSON,YAML)
Takes care of automatically rendering the response and converting it to appropriate format (text,XML,JSON,YAML)
depending on what the caller can accept. Returns Response
depending on what the caller can accept. Returns Response
"""
"""
if
isinstance
(
response
,
str
):
if
isinstance
(
response
,
Response
):
return
Response
(
code
,
response
,{
HttpHeader
.
CONTENT_TYPE
:
MediaType
.
TEXT_PLAIN
})
elif
isinstance
(
response
,
Response
):
return
response
return
response
else
:
else
:
(
content
,
contentType
)
=
self
.
__convertObjectToContentType
(
request
,
response
)
(
content
,
contentType
)
=
self
.
__convertObjectToContentType
(
request
,
response
)
...
@@ -318,20 +331,27 @@ class RequestRouter:
...
@@ -318,20 +331,27 @@ class RequestRouter:
Takes care of converting an object (non-String) response to the appropriate format, based on the what the caller can accept.
Takes care of converting an object (non-String) response to the appropriate format, based on the what the caller can accept.
Returns a tuple of (content,contentType)
Returns a tuple of (content,contentType)
"""
"""
obj
=
convertForSerialization
(
obj
)
if
HttpHeader
.
ACCEPT
in
request
.
received_headers
:
if
HttpHeader
.
ACCEPT
in
request
.
received_headers
:
accept
=
request
.
received_headers
[
HttpHeader
.
ACCEPT
]
accept
=
request
.
received_headers
[
HttpHeader
.
ACCEPT
]
if
MediaType
.
APPLICATION_JSON
in
accept
:
if
MediaType
.
APPLICATION_JSON
in
accept
:
if
not
advanced_json
:
obj
=
convertForSerialization
(
obj
)
return
(
convertToJson
(
obj
),
MediaType
.
APPLICATION_JSON
)
return
(
convertToJson
(
obj
),
MediaType
.
APPLICATION_JSON
)
elif
MediaType
.
TEXT_YAML
in
accept
:
elif
MediaType
.
TEXT_YAML
in
accept
:
obj
=
convertForSerialization
(
obj
)
return
(
yaml
.
dump
(
obj
),
MediaType
.
TEXT_YAML
)
return
(
yaml
.
dump
(
obj
),
MediaType
.
TEXT_YAML
)
elif
MediaType
.
APPLICATION_XML
in
accept
or
MediaType
.
TEXT_XML
in
accept
:
elif
MediaType
.
APPLICATION_XML
in
accept
or
MediaType
.
TEXT_XML
in
accept
:
obj
=
convertForSerialization
(
obj
)
return
(
generateXml
(
obj
),
MediaType
.
APPLICATION_XML
)
return
(
generateXml
(
obj
),
MediaType
.
APPLICATION_XML
)
else
:
else
:
# no idea, let's do JSON
# no idea, let's do JSON
if
not
advanced_json
:
obj
=
convertForSerialization
(
obj
)
return
(
convertToJson
(
obj
),
MediaType
.
APPLICATION_JSON
)
return
(
convertToJson
(
obj
),
MediaType
.
APPLICATION_JSON
)
else
:
else
:
if
not
advanced_json
:
obj
=
convertForSerialization
(
obj
)
# called has no accept header, let's default to JSON
# called has no accept header, let's default to JSON
return
(
convertToJson
(
obj
),
MediaType
.
APPLICATION_JSON
)
return
(
convertToJson
(
obj
),
MediaType
.
APPLICATION_JSON
)
...
@@ -361,19 +381,20 @@ class RequestRouter:
...
@@ -361,19 +381,20 @@ class RequestRouter:
'''Automatically parses JSON,XML,YAML if present'''
'''Automatically parses JSON,XML,YAML if present'''
if
request
.
method
in
(
Http
.
POST
,
Http
.
PUT
)
and
HttpHeader
.
CONTENT_TYPE
in
request
.
received_headers
.
keys
():
if
request
.
method
in
(
Http
.
POST
,
Http
.
PUT
)
and
HttpHeader
.
CONTENT_TYPE
in
request
.
received_headers
.
keys
():
contentType
=
request
.
received_headers
[
"content-type"
]
contentType
=
request
.
received_headers
[
"content-type"
]
data
=
request
.
content
.
read
()
if
contentType
==
MediaType
.
APPLICATION_JSON
:
if
contentType
==
MediaType
.
APPLICATION_JSON
:
try
:
try
:
request
.
json
=
json
.
loads
(
request
.
content
.
read
())
request
.
json
=
json
.
loads
(
data
)
if
data
else
{}
except
Exception
as
ex
:
except
Exception
as
ex
:
raise
TypeError
(
"Unable to parse JSON body:
%
s"
%
ex
)
raise
TypeError
(
"Unable to parse JSON body:
%
s"
%
ex
)
elif
contentType
in
(
MediaType
.
APPLICATION_XML
,
MediaType
.
TEXT_XML
):
elif
contentType
in
(
MediaType
.
APPLICATION_XML
,
MediaType
.
TEXT_XML
):
try
:
try
:
request
.
xml
=
ElementTree
.
XML
(
request
.
content
.
read
()
)
request
.
xml
=
ElementTree
.
XML
(
data
)
except
Exception
as
ex
:
except
Exception
as
ex
:
raise
TypeError
(
"Unable to parse XML body:
%
s"
%
ex
)
raise
TypeError
(
"Unable to parse XML body:
%
s"
%
ex
)
elif
contentType
==
MediaType
.
TEXT_YAML
:
elif
contentType
==
MediaType
.
TEXT_YAML
:
try
:
try
:
request
.
yaml
=
yaml
.
safe_load
(
request
.
content
.
read
()
)
request
.
yaml
=
yaml
.
safe_load
(
data
)
except
Exception
as
ex
:
except
Exception
as
ex
:
raise
TypeError
(
"Unable to parse YAML body:
%
s"
%
ex
)
raise
TypeError
(
"Unable to parse YAML body:
%
s"
%
ex
)
...
...
corepost/utils.py
View file @
4ecf20e7
...
@@ -2,7 +2,7 @@
...
@@ -2,7 +2,7 @@
Various CorePost utilities
Various CorePost utilities
'''
'''
from
inspect
import
getargspec
from
inspect
import
getargspec
import
json
def
getMandatoryArgumentNames
(
f
):
def
getMandatoryArgumentNames
(
f
):
'''Returns a tuple of the mandatory arguments required in a function'''
'''Returns a tuple of the mandatory arguments required in a function'''
...
@@ -12,16 +12,11 @@ def getMandatoryArgumentNames(f):
...
@@ -12,16 +12,11 @@ def getMandatoryArgumentNames(f):
else
:
else
:
return
args
[
0
:
len
(
args
)
-
len
(
defaults
)]
return
args
[
0
:
len
(
args
)
-
len
(
defaults
)]
def
getRouterKey
(
method
,
url
):
def
getRouterKey
(
method
,
url
):
'''Returns the common key used to represent a function that a request can be routed to'''
'''Returns the common key used to represent a function that a request can be routed to'''
return
"
%
s
%
s"
%
(
method
,
url
)
return
"
%
s
%
s"
%
(
method
,
url
)
def
convertToJson
(
obj
):
"""Converts to JSON, including Python classes that are not JSON serializable by default"""
try
:
return
json
.
dumps
(
obj
)
except
Exception
as
ex
:
raise
RuntimeError
(
str
(
ex
))
def
checkExpectedInterfaces
(
objects
,
expectedInterface
):
def
checkExpectedInterfaces
(
objects
,
expectedInterface
):
"""Verifies that all the objects implement the expected interface"""
"""Verifies that all the objects implement the expected interface"""
...
@@ -33,4 +28,3 @@ def safeDictUpdate(dictObject,key,value):
...
@@ -33,4 +28,3 @@ def safeDictUpdate(dictObject,key,value):
"""Only adds a key to a dictionary. If key exists, it leaves it untouched"""
"""Only adds a key to a dictionary. If key exists, it leaves it untouched"""
if
key
not
in
dictObject
:
if
key
not
in
dictObject
:
dictObject
[
key
]
=
value
dictObject
[
key
]
=
value
\ No newline at end of file
Write
Preview
Markdown
is supported
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