Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
I
iotgateway
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Analytics
Analytics
CI / CD
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
0Tyler
iotgateway
Commits
1a3f8353
Commit
1a3f8353
authored
May 23, 2019
by
0Tyler
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
gateway:DeviceService:addfilter & gateway:PrivacyService:addfilter
parent
0ce4516b
Changes
12
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
468 additions
and
435 deletions
+468
-435
.idea/workspace.xml
.idea/workspace.xml
+377
-390
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/config/DefaultData.java
.../edu/prlab/tyler/iotgateway/cloud/config/DefaultData.java
+2
-2
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/controllers/DeviceController.java
.../tyler/iotgateway/cloud/controllers/DeviceController.java
+7
-1
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/controllers/DocumentController.java
...yler/iotgateway/cloud/controllers/DocumentController.java
+4
-2
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/controllers/GatewayController.java
...tyler/iotgateway/cloud/controllers/GatewayController.java
+4
-0
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/util/CodeTools.java
...java/edu/prlab/tyler/iotgateway/cloud/util/CodeTools.java
+2
-1
contract/src/test/java/edu/prlab/tyler/iotgateway/contract/ContractTester.java
...a/edu/prlab/tyler/iotgateway/contract/ContractTester.java
+36
-8
gateway/src/main/java/edu/prlab/tyler/iotgateway/gateway/controllers/GatewayController.java
...ler/iotgateway/gateway/controllers/GatewayController.java
+5
-6
gateway/src/main/java/edu/prlab/tyler/iotgateway/gateway/repositories/DeviceIndexRepository.java
...otgateway/gateway/repositories/DeviceIndexRepository.java
+1
-0
gateway/src/main/java/edu/prlab/tyler/iotgateway/gateway/services/DeviceService.java
...rlab/tyler/iotgateway/gateway/services/DeviceService.java
+10
-2
gateway/src/main/java/edu/prlab/tyler/iotgateway/gateway/services/PrivacyService.java
...lab/tyler/iotgateway/gateway/services/PrivacyService.java
+7
-2
gateway/src/test/java/edu/prlab/tyler/iotgateway/gateway/GatewayHttpApiTest.java
...du/prlab/tyler/iotgateway/gateway/GatewayHttpApiTest.java
+13
-21
No files found.
.idea/workspace.xml
View file @
1a3f8353
This diff is collapsed.
Click to expand it.
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/config/DefaultData.java
View file @
1a3f8353
...
...
@@ -108,7 +108,7 @@ public class DefaultData implements ApplicationRunner {
.
build
();
Document
document
=
documentService
.
add
(
new
MockMultipartFile
(
"file"
,
"test.txt"
,
"text/plain"
,
"
testFileStirng
"
.
getBytes
()))
"text/plain"
,
"
This is fake IOT Device testing file.
"
.
getBytes
()))
.
orElse
(
Document
.
builder
().
build
());
PrivacyPolicyReport
oxygenPrivacyPolicyReport
=
PrivacyPolicyReport
.
builder
()
...
...
@@ -240,7 +240,7 @@ public class DefaultData implements ApplicationRunner {
.
build
();
document
=
documentService
.
add
(
new
MockMultipartFile
(
"file"
,
"test.txt"
,
"text/plain"
,
"
testFileStirng
"
.
getBytes
()))
"text/plain"
,
"
This is fake IOT Device testing file.
"
.
getBytes
()))
.
orElse
(
Document
.
builder
().
build
());
PrivacyPolicyReport
sensorPrivacyPolicyReport
=
PrivacyPolicyReport
.
builder
()
...
...
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/controllers/DeviceController.java
View file @
1a3f8353
...
...
@@ -5,6 +5,7 @@ import edu.prlab.tyler.iotgateway.cloud.pojo.privacy.PrivacyPolicyReport;
import
edu.prlab.tyler.iotgateway.cloud.services.DeviceService
;
import
edu.prlab.tyler.iotgateway.cloud.services.PrivacyPolicyReportService
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.http.CacheControl
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.web.bind.annotation.*
;
import
java.util.Optional
;
...
...
@@ -22,13 +23,15 @@ public class DeviceController {
this
.
privacyPolicyReportService
=
privacyPolicyReportService
;
}
//新增裝置
@PostMapping
(
"/device"
)
public
ResponseEntity
<
Device
>
addDevice
(
@RequestBody
Device
device
)
{
return
deviceService
.
add
(
device
)
.
map
(
ResponseEntity:
:
ok
)
.
orElseGet
(()->
ResponseEntity
.
noContent
().
build
());
.
orElseGet
(()->
ResponseEntity
.
noContent
().
cacheControl
(
CacheControl
.
noCache
()).
build
());
}
//讀取裝置
@GetMapping
(
"/device/{udn}"
)
public
ResponseEntity
<
Device
>
readDevice
(
@PathVariable
(
value
=
"udn"
,
required
=
false
)
String
udn
)
{
return
deviceService
.
readByUDN
(
udn
)
...
...
@@ -36,6 +39,7 @@ public class DeviceController {
.
orElseGet
(()->
ResponseEntity
.
noContent
().
build
());
}
//讀取所有裝置
@GetMapping
(
"/device"
)
public
ResponseEntity
<
Iterable
<
Device
>>
readDevices
()
{
return
Optional
.
of
(
deviceService
.
readll
())
...
...
@@ -43,6 +47,7 @@ public class DeviceController {
.
orElseGet
(()->
ResponseEntity
.
noContent
().
build
());
}
//新增PrivacyPolicyReport
@PostMapping
(
"/privacy"
)
public
ResponseEntity
<
PrivacyPolicyReport
>
addPrivacyPolicyReport
(
@RequestBody
PrivacyPolicyReport
privacyPolicy
)
{
return
privacyPolicyReportService
.
add
(
privacyPolicy
)
...
...
@@ -50,6 +55,7 @@ public class DeviceController {
.
orElseGet
(()->
ResponseEntity
.
noContent
().
build
());
}
//透過UDN讀取PrivacyPolicyReport
@GetMapping
(
"/privacy/{UDN}"
)
public
ResponseEntity
<
PrivacyPolicyReport
>
readPrivacyPolicyReportByDevice
(
@PathVariable
(
value
=
"UDN"
)
String
UDN
)
{
return
privacyPolicyReportService
.
readByDevice
(
UDN
)
...
...
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/controllers/DocumentController.java
View file @
1a3f8353
...
...
@@ -25,6 +25,7 @@ public class DocumentController {
this
.
privacyPolicyReportService
=
privacyPolicyReportService
;
}
//上傳檔案
@PostMapping
public
ResponseEntity
<
Document
>
uploadFile
(
@RequestPart
MultipartFile
file
)
throws
IOException
{
return
documentService
.
add
(
file
)
...
...
@@ -32,11 +33,12 @@ public class DocumentController {
.
orElseGet
(()
->
ResponseEntity
.
noContent
().
build
());
}
//透過裝置ID取得檔案
@GetMapping
(
"/{udn}"
)
public
ResponseEntity
<
ByteArrayResource
>
findFile
(
@PathVariable
String
udn
)
{
return
privacyPolicyReportService
.
readByDevice
(
udn
)
.
map
(
privacyReport
->
privacyReport
.
getDocument
().
getId
())
.
flatMap
(
id
->
documentService
.
readFile
(
id
))
.
map
(
privacyReport
->
privacyReport
.
getDocument
().
getId
())
.
flatMap
(
id
->
documentService
.
readFile
(
id
))
.
map
(
document
->
ResponseEntity
.
ok
()
.
contentType
(
MediaType
.
parseMediaType
(
document
.
getFileType
()))
.
header
(
HttpHeaders
.
CONTENT_DISPOSITION
,
"attachment; filename* = UTF-8''"
+
CodeTools
.
encode
(
document
.
getFileName
()))
...
...
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/controllers/GatewayController.java
View file @
1a3f8353
...
...
@@ -17,6 +17,7 @@ public class GatewayController {
this
.
service
=
service
;
}
//表達隱私偏好
@PostMapping
(
"/choice"
)
public
ResponseEntity
<
PrivacyChoice
>
setPrivacyChoice
(
@RequestBody
PrivacyChoice
privacyChoice
)
{
return
service
.
add
(
privacyChoice
)
...
...
@@ -24,6 +25,7 @@ public class GatewayController {
.
orElseGet
(()
->
ResponseEntity
.
noContent
().
build
());
}
//取得所有隱私偏好紀錄
@GetMapping
(
"/choice"
)
public
ResponseEntity
<
Iterable
<
PrivacyChoice
>>
readPrivacyChoice
()
{
return
Optional
.
of
(
service
.
readll
())
...
...
@@ -31,10 +33,12 @@ public class GatewayController {
.
orElseGet
(()
->
ResponseEntity
.
noContent
().
build
());
}
//透過裝置取得所有隱私偏好
@GetMapping
(
"/choice/{udn}"
)
public
ResponseEntity
<
Iterable
<
PrivacyChoice
>>
readPrivacyChoiceByDevice
(
@PathVariable
String
udn
)
{
return
Optional
.
ofNullable
(
service
.
readPrivacyChoiceByDevice
(
udn
))
.
map
(
ResponseEntity:
:
ok
)
.
orElseGet
(()
->
ResponseEntity
.
noContent
().
build
());
}
}
\ No newline at end of file
cloud/src/main/java/edu/prlab/tyler/iotgateway/cloud/util/CodeTools.java
View file @
1a3f8353
...
...
@@ -2,6 +2,7 @@ package edu.prlab.tyler.iotgateway.cloud.util;
import
java.io.UnsupportedEncodingException
;
import
java.net.URLEncoder
;
import
java.util.function.Function
;
public
class
CodeTools
{
private
CodeTools
()
{
...
...
@@ -15,5 +16,5 @@ public class CodeTools {
return
"Error: "
+
e
.
getMessage
();
}
}
}
contract/src/test/java/edu/prlab/tyler/iotgateway/contract/ContractTester.java
View file @
1a3f8353
...
...
@@ -7,7 +7,6 @@ import org.junit.FixMethodOrder;
import
org.junit.Test
;
import
org.junit.runners.MethodSorters
;
import
org.web3j.protocol.core.methods.response.TransactionReceipt
;
import
org.web3j.protocol.core.methods.response.Web3ClientVersion
;
import
org.web3j.protocol.http.HttpService
;
import
org.web3j.quorum.Quorum
;
import
org.web3j.tx.ClientTransactionManager
;
...
...
@@ -29,8 +28,8 @@ public class ContractTester {
DefaultGasProvider
.
GAS_LIMIT
);
private
String
deviceContractAddress
;
private
String
gateW
ayContractAddress
;
private
static
String
deviceContractAddress
;
private
static
String
gatew
ayContractAddress
;
@BeforeClass
public
static
void
setUp
()
{
...
...
@@ -41,32 +40,34 @@ public class ContractTester {
// Credentials credentials = Credentials.create(privateKey);
Quorum
quorum
=
Quorum
.
build
(
new
HttpService
(
rpcUrl
));
Web3ClientVersion
web3ClientVersion
=
quorum
.
web3ClientVersion
().
sendAsync
().
get
();
String
clientVersion
=
web3ClientVersion
.
getWeb3ClientVersion
();
Assert
.
assertNotNull
(
clientVersion
);
String
userAddress
=
quorum
.
ethAccounts
().
send
().
getAccounts
().
get
(
quorum
.
ethAccounts
().
send
().
getAccounts
().
size
()
-
1
);
Assert
.
assertNotNull
(
userAddress
);
ClientTransactionManager
manager
=
new
ClientTransactionManager
(
quorum
,
userAddress
);
//
Device
//
部屬裝置合約
DeviceContract
deviceContract
=
DeviceContract
.
deploy
(
quorum
,
manager
,
DEFAULT_GAS_PROVIDER
).
send
();
deviceContractAddress
=
deviceContract
.
getContractAddress
();
Assert
.
assertNotNull
(
deviceContract
);
Assert
.
assertNotNull
(
deviceContractAddress
);
System
.
out
.
println
(
"device 合約位置:"
+
deviceContractAddress
);
//設定裝置資訊
TransactionReceipt
receipt
;
receipt
=
deviceContract
.
setdeviceinfo
(
"testdevice"
).
send
();
Assert
.
assertNotNull
(
receipt
);
//設定隱私政策
receipt
=
deviceContract
.
setpp
(
"testpp"
).
send
();
Assert
.
assertNotNull
(
receipt
);
//取得裝置資訊
String
deviceInfo
=
deviceContract
.
deviceInfo
().
send
();
Assert
.
assertNotNull
(
deviceInfo
);
System
.
out
.
println
(
deviceInfo
);
//取得隱私政策
String
privacyPolicy
=
deviceContract
.
privacypolicy
().
send
();
Assert
.
assertNotNull
(
privacyPolicy
);
System
.
out
.
println
(
privacyPolicy
);
...
...
@@ -97,4 +98,31 @@ public class ContractTester {
// System.out.println("binded : " + bindedString);
}
@Test
public
void
test2Contract
()
throws
Exception
{
Quorum
quorum
=
Quorum
.
build
(
new
HttpService
(
rpcUrl
));
String
userAddress
=
quorum
.
ethAccounts
().
send
().
getAccounts
().
get
(
quorum
.
ethAccounts
().
send
().
getAccounts
().
size
()
-
1
);
Assert
.
assertNotNull
(
userAddress
);
ClientTransactionManager
manager
=
new
ClientTransactionManager
(
quorum
,
userAddress
);
GatewayContract
gatewayContract
=
GatewayContract
.
deploy
(
quorum
,
manager
,
DEFAULT_GAS_PROVIDER
).
send
();
gatewayContractAddress
=
gatewayContract
.
getContractAddress
();
Assert
.
assertNotNull
(
gatewayContract
);
Assert
.
assertNotNull
(
gatewayContractAddress
);
System
.
out
.
println
(
"gateway 合約位置:"
+
gatewayContractAddress
);
//根據合約地址取得合約實體
DeviceContract
deviceContract
=
DeviceContract
.
load
(
deviceContractAddress
,
quorum
,
manager
,
DEFAULT_GAS_PROVIDER
);
//設定Buyer
TransactionReceipt
receipt
=
deviceContract
.
setBuyer
(
gatewayContract
.
getContractAddress
()).
send
();
Assert
.
assertNotNull
(
receipt
);
System
.
out
.
println
(
receipt
);
receipt
=
gatewayContract
.
bindRequest
(
deviceContractAddress
).
send
();
Assert
.
assertNotNull
(
receipt
);
System
.
out
.
println
(
receipt
);
}
}
gateway/src/main/java/edu/prlab/tyler/iotgateway/gateway/controllers/GatewayController.java
View file @
1a3f8353
...
...
@@ -60,12 +60,11 @@ public class GatewayController {
.
orElseGet
(()
->
ResponseEntity
.
noContent
().
build
());
}
// TODO
//根據使用者取得在此gateway上該使用者的隱私選擇列表(根據時間排序),
//暫時從DB撈,無特定使用者
@GetMapping
(
"/choice"
)
public
ResponseEntity
<
Iterable
<
PrivacyChoiceResponse
>>
readPrivacyChoiceRecordsByUser
()
{
return
privacyService
.
getPrivacyPolicyChoices
()
// TODO 針對使用者取得隱私選擇列表
//根據使用者取得在此gateway上該使用者的隱私選擇列表
@GetMapping
(
"/choice/{account}"
)
public
ResponseEntity
<
Iterable
<
PrivacyChoiceResponse
>>
readPrivacyChoiceRecordsByUser
(
@PathVariable
String
account
)
{
return
privacyService
.
getPrivacyPolicyChoicesByAccount
(
account
)
.
map
(
ResponseEntity:
:
ok
)
.
orElseGet
(()
->
ResponseEntity
.
noContent
().
build
());
}
...
...
gateway/src/main/java/edu/prlab/tyler/iotgateway/gateway/repositories/DeviceIndexRepository.java
View file @
1a3f8353
...
...
@@ -6,4 +6,5 @@ import org.springframework.stereotype.Repository;
@Repository
public
interface
DeviceIndexRepository
extends
CrudRepository
<
DeviceIndex
,
String
>
{
boolean
findByUdnContains
(
String
udn
);
}
gateway/src/main/java/edu/prlab/tyler/iotgateway/gateway/services/DeviceService.java
View file @
1a3f8353
package
edu.prlab.tyler.iotgateway.gateway.services
;
import
edu.prlab.tyler.iotgateway.cloud.pojo.device.Device
;
import
edu.prlab.tyler.iotgateway.gateway.pojo.DeviceIndex
;
import
edu.prlab.tyler.iotgateway.gateway.repositories.DeviceIndexRepository
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
import
java.util.Optional
;
import
java.util.stream.Collectors
;
import
java.util.stream.StreamSupport
;
@Service
public
class
DeviceService
{
...
...
@@ -21,11 +24,16 @@ public class DeviceService {
//TODO bind and read from blockchain
public
Optional
<
Device
>
bindDeviceAndGateway
(
String
udn
)
{
return
remoteService
.
readDevice
(
udn
);
return
Optional
.
of
(
deviceIndexRepository
.
save
(
DeviceIndex
.
builder
()
.
udn
(
udn
).
build
()))
.
flatMap
(
deviceIndex
->
remoteService
.
readDevice
(
udn
));
}
public
Optional
<
Iterable
<
Device
>>
readDevices
()
{
return
remoteService
.
readDevices
();
return
remoteService
.
readDevices
()
.
map
(
devices
->
StreamSupport
.
stream
(
devices
.
spliterator
(),
false
)
.
filter
(
device
->
deviceIndexRepository
.
existsById
(
device
.
getUdn
()))
.
collect
(
Collectors
.
toList
()));
}
}
\ No newline at end of file
gateway/src/main/java/edu/prlab/tyler/iotgateway/gateway/services/PrivacyService.java
View file @
1a3f8353
...
...
@@ -5,6 +5,7 @@ import edu.prlab.tyler.iotgateway.cloud.pojo.privacy.PrivacyPolicyReport;
import
edu.prlab.tyler.iotgateway.gateway.model.PrivacyChoiceResponse
;
import
edu.prlab.tyler.iotgateway.gateway.pojo.PrivacyChoiceIndex
;
import
edu.prlab.tyler.iotgateway.gateway.repositories.PrivacyChoiceIndexRepository
;
import
edu.prlab.tyler.iotgateway.gateway.repositories.UserRepository
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
...
...
@@ -16,11 +17,14 @@ import java.util.stream.StreamSupport;
@Service
public
class
PrivacyService
{
private
PrivacyChoiceIndexRepository
privacyChoiceIndexRepository
;
private
UserRepository
userRepository
;
private
RemoteService
remoteService
;
@Autowired
public
PrivacyService
(
PrivacyChoiceIndexRepository
privacyChoiceIndexRepository
,
RemoteService
remoteService
)
{
public
PrivacyService
(
PrivacyChoiceIndexRepository
privacyChoiceIndexRepository
,
RemoteService
remoteService
,
UserRepository
userRepository
)
{
this
.
privacyChoiceIndexRepository
=
privacyChoiceIndexRepository
;
this
.
userRepository
=
userRepository
;
this
.
remoteService
=
remoteService
;
}
...
...
@@ -48,9 +52,10 @@ public class PrivacyService {
// new ParameterizedTypeReference<Iterable<PrivacyChoice>>() { }).getBody());
// }
public
Optional
<
Iterable
<
PrivacyChoiceResponse
>>
getPrivacyPolicyChoices
(
)
{
public
Optional
<
Iterable
<
PrivacyChoiceResponse
>>
getPrivacyPolicyChoices
ByAccount
(
String
account
)
{
return
remoteService
.
readPrivacyChoices
()
.
map
(
choices
->
StreamSupport
.
stream
(
choices
.
spliterator
(),
false
)
.
filter
(
privacyChoice
->
privacyChoice
.
getPrivacyContent
().
getUser
().
getAccount
().
equals
(
account
))
.
map
(
choice
->
privacyChoiceIndexRepository
.
findById
(
choice
.
getId
())
.
map
(
choiceIndex
->
PrivacyChoiceResponse
.
builder
()
.
id
(
choiceIndex
.
getId
())
...
...
gateway/src/test/java/edu/prlab/tyler/iotgateway/gateway/GatewayHttpApiTest.java
View file @
1a3f8353
...
...
@@ -57,18 +57,6 @@ public class GatewayHttpApiTest {
.
andReturn
();
Assert
.
assertNotNull
(
result
);
//取得裝置清單
result
=
mvc
.
perform
(
MockMvcRequestBuilders
.
get
(
"/device"
)
.
accept
(
MediaType
.
APPLICATION_JSON_UTF8
))
.
andDo
(
print
())
.
andExpect
(
status
().
isOk
())
.
andReturn
();
Iterable
<
Device
>
devices
=
mapper
.
readValue
(
result
.
getResponse
().
getContentAsString
(),
new
TypeReference
<
Iterable
<
Device
>>()
{
});
Assert
.
assertNotNull
(
devices
);
//綁定裝置
result
=
mvc
.
perform
(
MockMvcRequestBuilders
.
post
(
"/device/"
+
"a1252c49-4188-4e6d-a32e-66604c664fb8"
)
...
...
@@ -81,6 +69,18 @@ public class GatewayHttpApiTest {
Device
device
=
mapper
.
readValue
(
result
.
getResponse
().
getContentAsString
(),
Device
.
class
);
Assert
.
assertNotNull
(
device
);
//取得裝置清單
result
=
mvc
.
perform
(
MockMvcRequestBuilders
.
get
(
"/device"
)
.
accept
(
MediaType
.
APPLICATION_JSON_UTF8
))
.
andDo
(
print
())
.
andExpect
(
status
().
isOk
())
.
andReturn
();
Iterable
<
Device
>
devices
=
mapper
.
readValue
(
result
.
getResponse
().
getContentAsString
(),
new
TypeReference
<
Iterable
<
Device
>>()
{
});
Assert
.
assertNotNull
(
devices
);
//拿取隱私政策
result
=
mvc
.
perform
(
MockMvcRequestBuilders
.
get
(
"/privacy/"
+
"a1252c49-4188-4e6d-a32e-66604c664fb8"
)
...
...
@@ -117,15 +117,7 @@ public class GatewayHttpApiTest {
//取得所有隱私偏好
result
=
mvc
.
perform
(
MockMvcRequestBuilders
.
get
(
"/choice"
)
.
accept
(
MediaType
.
APPLICATION_JSON_UTF8
))
.
andDo
(
print
())
.
andExpect
(
status
().
isOk
())
.
andReturn
();
Assert
.
assertNotNull
(
result
);
result
=
mvc
.
perform
(
MockMvcRequestBuilders
.
get
(
"/choice"
)
.
get
(
"/choice"
+
"/test1"
)
.
accept
(
MediaType
.
APPLICATION_JSON_UTF8
))
.
andDo
(
print
())
.
andExpect
(
status
().
isOk
())
...
...
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