- added SQLite back end
- reworked graphical representation to use widgets
This commit is contained in:
@ -0,0 +1,31 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2012 IT Just working.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IT Just working - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.minres.scviewer.database.text
|
||||
|
||||
import com.minres.scviewer.database.AssociationType;
|
||||
import com.minres.scviewer.database.DataType;
|
||||
import com.minres.scviewer.database.ITrAttrType;
|
||||
|
||||
class AttrType implements ITrAttrType {
|
||||
String name
|
||||
DataType dataType
|
||||
AssociationType type
|
||||
|
||||
static AttrType getAttrType(String name, DataType dataType, AssociationType type){
|
||||
AttrTypeFactory.instance.getAttrType(name, dataType, type)
|
||||
}
|
||||
|
||||
AttrType(String name, DataType dataType, AssociationType type){
|
||||
this.name=name
|
||||
this.dataType=dataType
|
||||
this.type=type
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2012 IT Just working.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IT Just working - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.minres.scviewer.database.text
|
||||
|
||||
import com.minres.scviewer.database.AssociationType;
|
||||
import com.minres.scviewer.database.DataType
|
||||
import com.minres.scviewer.database.ITrAttrType
|
||||
import com.minres.scviewer.database.ITrAttribute
|
||||
|
||||
class AttrTypeFactory {
|
||||
private static final instance = new AttrTypeFactory()
|
||||
|
||||
def attributes = [:]
|
||||
|
||||
private AttrTypeFactory() {
|
||||
AttrTypeFactory.metaClass.constructor = {-> instance }
|
||||
}
|
||||
|
||||
ITrAttrType getAttrType(String name, DataType dataType, AssociationType type){
|
||||
def key = name+":"+dataType.toString()
|
||||
ITrAttrType res
|
||||
if(attributes.containsKey(key)){
|
||||
res=attributes[key]
|
||||
} else {
|
||||
res=new AttrType(name, dataType, type)
|
||||
attributes[key]=res
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
@ -0,0 +1,59 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2012 IT Just working.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IT Just working - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.minres.scviewer.database.text
|
||||
|
||||
import com.minres.scviewer.database.AssociationType;
|
||||
import com.minres.scviewer.database.DataType;
|
||||
import com.minres.scviewer.database.ITrAttrType;
|
||||
import com.minres.scviewer.database.ITrAttribute
|
||||
|
||||
class Attribute implements ITrAttribute{
|
||||
|
||||
AttrType attributeType
|
||||
|
||||
def value
|
||||
|
||||
Attribute(String name, DataType dataType, AssociationType type, value){
|
||||
attributeType = AttrTypeFactory.instance.getAttrType(name, dataType, type)
|
||||
switch(dataType){
|
||||
case DataType.STRING:
|
||||
case DataType.ENUMERATION:
|
||||
this.value=value[1..-2]
|
||||
break;
|
||||
default:
|
||||
this.value=value
|
||||
}
|
||||
}
|
||||
|
||||
Attribute(AttrType other){
|
||||
attributeType=other
|
||||
}
|
||||
|
||||
Attribute(AttrType other, value){
|
||||
this(other.name, other.dataType, other.type, value)
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return attributeType.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssociationType getType() {
|
||||
attributeType.type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataType getDataType() {
|
||||
attributeType.dataType;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2012 IT Just working.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IT Just working - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.minres.scviewer.database.text
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.minres.scviewer.database.ITrAttrType
|
||||
import com.minres.scviewer.database.ITrAttribute;
|
||||
import com.minres.scviewer.database.ITrGenerator;
|
||||
import com.minres.scviewer.database.ITrStream;
|
||||
import com.minres.scviewer.database.ITransaction;
|
||||
|
||||
class Generator implements ITrGenerator{
|
||||
Long id
|
||||
Stream stream
|
||||
String name
|
||||
Boolean active = false
|
||||
ArrayList<ITransaction> transactions=[]
|
||||
|
||||
ArrayList<ITrAttrType> begin_attrs = []
|
||||
int begin_attrs_idx = 0
|
||||
ArrayList<ITrAttrType> end_attrs= []
|
||||
int end_attrs_idx = 0
|
||||
|
||||
Generator(int id, Stream stream, name){
|
||||
this.id=id
|
||||
this.stream=stream
|
||||
this.name=name
|
||||
}
|
||||
|
||||
ITrStream getStream(){
|
||||
return stream;
|
||||
}
|
||||
|
||||
List<ITransaction> getTransactions(){
|
||||
return transactions
|
||||
}
|
||||
|
||||
Boolean isActive() {return active};
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2012 IT Just working.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IT Just working - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.minres.scviewer.database.text;
|
||||
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.minres.scviewer.database.ITrHierNode;
|
||||
|
||||
class HierNode implements ITrHierNode {
|
||||
|
||||
String name
|
||||
def childs = []
|
||||
|
||||
public HierNode(){
|
||||
}
|
||||
|
||||
public HierNode(String name){
|
||||
this.name=name
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ITrHierNode> getChildNodes() {
|
||||
return childs.sort{it.name}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFullName() {
|
||||
// TODO Auto-generated method stub
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPropertyChangeListener(PropertyChangeListener l) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePropertyChangeListener(PropertyChangeListener l) {
|
||||
// TODO Auto-generated method stub
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.minres.scviewer.database.text
|
||||
|
||||
import com.minres.scviewer.database.ITrRelation
|
||||
import com.minres.scviewer.database.ITransaction;
|
||||
import com.minres.scviewer.database.RelationType;
|
||||
|
||||
class Relation implements ITrRelation {
|
||||
Transaction source
|
||||
|
||||
Transaction target
|
||||
|
||||
RelationType relationType
|
||||
|
||||
|
||||
public Relation(RelationType relationType, Transaction source, Transaction target) {
|
||||
this.source = source;
|
||||
this.target = target;
|
||||
this.relationType = relationType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RelationType getRelationType() {
|
||||
return relationType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITransaction getSource() {
|
||||
return source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITransaction getTarget() {
|
||||
return target;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2012 IT Just working.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IT Just working - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.minres.scviewer.database.text
|
||||
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.minres.scviewer.database.ITrDb
|
||||
import com.minres.scviewer.database.ITrGenerator
|
||||
import com.minres.scviewer.database.ITrHierNode
|
||||
import com.minres.scviewer.database.ITrStream
|
||||
import com.minres.scviewer.database.ITransaction
|
||||
|
||||
class Stream extends HierNode implements ITrStream {
|
||||
|
||||
Long id;
|
||||
|
||||
TextDb database
|
||||
|
||||
String fullName;
|
||||
|
||||
String kind;
|
||||
|
||||
def generators = [];
|
||||
|
||||
private allTransactions;
|
||||
|
||||
public TrHierNode(String name){
|
||||
this.name=name
|
||||
}
|
||||
|
||||
Stream(int id, TextDb db, String name, String kind){
|
||||
this.id=id
|
||||
this.database=db
|
||||
this.name=name
|
||||
this.fullName=name
|
||||
this.kind=kind
|
||||
}
|
||||
|
||||
List<ITrGenerator> getGenerators(){
|
||||
return generators as List<ITrGenerator>
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITrDb getDb() {
|
||||
return database;
|
||||
}
|
||||
|
||||
// FIXME: maybe need to be somewhere else
|
||||
public int getMaxConcurrrentTx() {
|
||||
def rowendtime = [0]
|
||||
getTransactions().each{Transaction tx ->
|
||||
def rowIdx = 0
|
||||
for(rowIdx=0; rowendtime.size()<rowIdx || rowendtime[rowIdx]>tx.beginTime.value; rowIdx++);
|
||||
if(rowendtime.size<=rowIdx){
|
||||
rowendtime<<tx.endTime?.value?:tx.beginTime.value+1
|
||||
} else {
|
||||
rowendtime[rowIdx]=tx.endTime?.value?:tx.beginTime.value+1
|
||||
}
|
||||
}
|
||||
return rowendtime.size()
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ITransaction> getTransactions() {
|
||||
if(!allTransactions)
|
||||
allTransactions=generators.transactions.flatten().sort{it.beginTime.value}
|
||||
return allTransactions
|
||||
}
|
||||
|
||||
@Override
|
||||
public ITransaction getTransactionById(long id) {
|
||||
if(!allTransactions)
|
||||
allTransactions=generators.transactions.flatten().sort{it.beginTime.value}
|
||||
allTransactions.find{it.id==id}
|
||||
}
|
||||
}
|
@ -0,0 +1,190 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2012 IT Just working.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IT Just working - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.minres.scviewer.database.text;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import com.minres.scviewer.database.AssociationType;
|
||||
import com.minres.scviewer.database.DataType;
|
||||
import com.minres.scviewer.database.ITrAttrType;
|
||||
import com.minres.scviewer.database.ITrAttribute;
|
||||
import com.minres.scviewer.database.ITrDb;
|
||||
import com.minres.scviewer.database.ITrGenerator;
|
||||
import com.minres.scviewer.database.ITrHierNode;
|
||||
import com.minres.scviewer.database.ITrStream;
|
||||
import com.minres.scviewer.database.InputFormatException;
|
||||
import com.minres.scviewer.database.EventTime
|
||||
import com.minres.scviewer.database.RelationType
|
||||
|
||||
public class TextDb extends HierNode implements ITrDb{
|
||||
|
||||
private EventTime maxTime;
|
||||
|
||||
def streams = []
|
||||
|
||||
def relationTypes=[:]
|
||||
|
||||
public String getFullName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventTime getMaxTime() {
|
||||
return maxTime;
|
||||
}
|
||||
|
||||
public ITrStream getStreamByName(String name){
|
||||
streams.find{ITrStream stream-> stream.fullName == name }
|
||||
}
|
||||
|
||||
public List<ITrStream> getAllStreams() {
|
||||
return new LinkedList<ITrStream>(streams);
|
||||
}
|
||||
|
||||
public List<ITrHierNode> getChildNodes() {
|
||||
return childs.sort{it.name};
|
||||
}
|
||||
|
||||
public Map<Long, ITrGenerator> getGeneratorsById() {
|
||||
TreeMap<Long, ITrGenerator> res = new TreeMap<Long, ITrGenerator>();
|
||||
streams.each{Stream stream -> stream.generators.each{res.put(it.id, id)} }
|
||||
return res;
|
||||
}
|
||||
|
||||
void clear(){
|
||||
streams = []
|
||||
maxTime=new EventTime(0, "ns")
|
||||
}
|
||||
|
||||
void load(File file) throws InputFormatException {
|
||||
this.name = file.name;
|
||||
parseInput(file)
|
||||
}
|
||||
|
||||
private def parseInput(File input){
|
||||
def streamsById = [:]
|
||||
def generatorsById = [:]
|
||||
def transactionsById = [:]
|
||||
Generator generator
|
||||
Transaction transaction
|
||||
boolean endTransaction=false
|
||||
def matcher
|
||||
input.eachLine { line ->
|
||||
def tokens = line.split(/\s+/)
|
||||
switch(tokens[0]){
|
||||
case "scv_tr_stream":
|
||||
case "scv_tr_generator":
|
||||
case "begin_attribute":
|
||||
case "end_attribute":
|
||||
if ((matcher = line =~ /^scv_tr_stream\s+\(ID (\d+),\s+name\s+"([^"]+)",\s+kind\s+"([^"]+)"\)$/)) {
|
||||
def id = Integer.parseInt(matcher[0][1])
|
||||
def stream = new Stream(id, this, matcher[0][2], matcher[0][3])
|
||||
streams<<stream
|
||||
streamsById[id]=stream
|
||||
} else if ((matcher = line =~ /^scv_tr_generator\s+\(ID\s+(\d+),\s+name\s+"([^"]+)",\s+scv_tr_stream\s+(\d+),$/)) {
|
||||
def id = Integer.parseInt(matcher[0][1])
|
||||
ITrStream stream=streamsById[Integer.parseInt(matcher[0][3])]
|
||||
generator=new Generator(id, stream, matcher[0][2])
|
||||
stream.generators<<generator
|
||||
generatorsById[id]=generator
|
||||
} else if ((matcher = line =~ /^begin_attribute \(ID (\d+), name "([^"]+)", type "([^"]+)"\)$/)) {
|
||||
generator.begin_attrs << AttrType.getAttrType(matcher[0][2], DataType.valueOf(matcher[0][3]), AssociationType.BEGIN)
|
||||
} else if ((matcher = line =~ /^end_attribute \(ID (\d+), name "([^"]+)", type "([^"]+)"\)$/)) {
|
||||
generator.end_attrs << AttrType.getAttrType(matcher[0][2], DataType.valueOf(matcher[0][3]), AssociationType.END)
|
||||
}
|
||||
break;
|
||||
case ")":
|
||||
generator=null
|
||||
break
|
||||
case "tx_begin"://matcher = line =~ /^tx_begin\s+(\d+)\s+(\d+)\s+(\d+)\s+([munpf]?s)/
|
||||
def id = Integer.parseInt(tokens[1])
|
||||
Generator gen=generatorsById[Integer.parseInt(tokens[2])]
|
||||
transaction = new Transaction(id, gen.stream, gen, new EventTime(Integer.parseInt(tokens[3]), tokens[4]))
|
||||
gen.transactions << transaction
|
||||
transactionsById[id]= transaction
|
||||
gen.begin_attrs_idx=0;
|
||||
maxTime = maxTime>transaction.beginTime?maxTime:transaction.beginTime
|
||||
endTransaction=false
|
||||
break
|
||||
case "tx_end"://matcher = line =~ /^tx_end\s+(\d+)\s+(\d+)\s+(\d+)\s+([munpf]?s)/
|
||||
def id = Integer.parseInt(tokens[1])
|
||||
transaction = transactionsById[id]
|
||||
assert Integer.parseInt(tokens[2])==transaction.generator.id
|
||||
transaction.endTime = new EventTime(Integer.parseInt(tokens[3]), tokens[4])
|
||||
transaction.generator.end_attrs_idx=0;
|
||||
maxTime = maxTime>transaction.endTime?maxTime:transaction.endTime
|
||||
endTransaction=true
|
||||
break
|
||||
case "tx_record_attribute"://matcher = line =~ /^tx_record_attribute\s+(\d+)\s+"([^"]+)"\s+(\S+)\s*=\s*(.+)$/
|
||||
def id = Integer.parseInt(tokens[1])
|
||||
transactionsById[id].attributes<<new Attribute(tokens[2][1..-2], DataType.valueOf(tokens[3]), AssociationType.RECORD, tokens[5..-1].join(' '))
|
||||
break
|
||||
case "a"://matcher = line =~ /^a\s+(.+)$/
|
||||
if(endTransaction){
|
||||
transaction.attributes << new Attribute(transaction.generator.end_attrs[0], tokens[1])
|
||||
} else {
|
||||
transaction.attributes << new Attribute(transaction.generator.begin_attrs[0], tokens[1])
|
||||
}
|
||||
break
|
||||
case "tx_relation"://matcher = line =~ /^tx_relation\s+\"(\S+)\"\s+(\d+)\s+(\d+)$/
|
||||
Transaction tr1= transactionsById[Integer.parseInt(tokens[2])]
|
||||
Transaction tr2= transactionsById[Integer.parseInt(tokens[3])]
|
||||
def relType=tokens[1][1..-2]
|
||||
if(!relationTypes.containsKey(relType)) relationTypes[relType]=new RelationType(relType)
|
||||
def rel = new Relation(relationTypes[relType], tr1, tr2)
|
||||
tr1.outgoingRelations<<rel
|
||||
tr2.incomingRelations<<rel
|
||||
break
|
||||
default:
|
||||
println "Don't know what to do with: '$line'"
|
||||
|
||||
}
|
||||
}
|
||||
addHierarchyNodes()
|
||||
}
|
||||
|
||||
def addHierarchyNodes(){
|
||||
streams.each{ Stream stream->
|
||||
def hier = stream.fullName.split(/\./)
|
||||
ITrHierNode node = this
|
||||
hier.each { name ->
|
||||
def n1 = node.childNodes.find{it.name == name}
|
||||
if(name == hier[-1]){ //leaf
|
||||
if(n1!=null) {
|
||||
if(n1 instanceof HierNode){
|
||||
node.childNodes.remove(n1)
|
||||
stream.childNodes.addAll(n1.childNodes)
|
||||
} else {
|
||||
throw new InputFormatException()
|
||||
}
|
||||
}
|
||||
stream.name=name
|
||||
node.childNodes<<stream
|
||||
node=stream
|
||||
} else { // intermediate
|
||||
if(n1 != null) {
|
||||
node=n1
|
||||
} else {
|
||||
HierNode newNode = new HierNode(name)
|
||||
node.childNodes<<newNode
|
||||
node=newNode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package com.minres.scviewer.database.text
|
||||
|
||||
import java.io.FileInputStream;
|
||||
|
||||
import com.minres.scviewer.database.ITrDb
|
||||
import com.minres.scviewer.database.ITransactionDbFactory;
|
||||
|
||||
class TextDbFactory implements ITransactionDbFactory {
|
||||
|
||||
byte[] x = "scv_tr_stream".bytes
|
||||
|
||||
@Override
|
||||
public ITrDb createDatabase(File file) {
|
||||
try {
|
||||
FileInputStream fis = new FileInputStream(file);
|
||||
byte[] buffer = new byte[x.size()];
|
||||
def read = fis.read(buffer, 0, x.size());
|
||||
fis.close();
|
||||
if(read==x.size())
|
||||
for(int i=0; i<x.size(); i++)
|
||||
if(buffer[i]!=x[i]) return null;
|
||||
def db = new TextDb();
|
||||
db.load(file)
|
||||
return db
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (c) 2012 IT Just working.
|
||||
* All rights reserved. This program and the accompanying materials
|
||||
* are made available under the terms of the Eclipse Public License v1.0
|
||||
* which accompanies this distribution, and is available at
|
||||
* http://www.eclipse.org/legal/epl-v10.html
|
||||
*
|
||||
* Contributors:
|
||||
* IT Just working - initial API and implementation
|
||||
*******************************************************************************/
|
||||
package com.minres.scviewer.database.text
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Set
|
||||
|
||||
import com.minres.scviewer.database.*
|
||||
|
||||
class Transaction implements ITransaction {
|
||||
|
||||
Long id
|
||||
|
||||
Generator generator
|
||||
|
||||
Stream stream
|
||||
|
||||
EventTime beginTime
|
||||
|
||||
EventTime endTime
|
||||
|
||||
ArrayList<ITrAttribute> attributes = new ArrayList<ITrAttribute>()
|
||||
|
||||
def incomingRelations =[]
|
||||
|
||||
def outgoingRelations =[]
|
||||
|
||||
Transaction(int id, Stream stream, Generator generator, EventTime begin){
|
||||
this.id=id
|
||||
this.generator=generator
|
||||
this.beginTime=begin
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ITrRelation> getIncomingRelations() {
|
||||
return incomingRelations;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<ITrRelation> getOutgoingRelations() {
|
||||
return outgoingRelations;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user