提交 f557cc69 编写于 作者: J jurgen

Complex type model refactoring

Support of reference data type
Complex object editor improved
上级 ab77730a
......@@ -1791,6 +1791,7 @@
<type standard="STRUCT"/>
<type standard="ARRAY"/>
<type standard="REF"/>
<type standard="BLOB"/>
<type standard="CLOB"/>
......
......@@ -30,8 +30,6 @@ import org.jkiss.dbeaver.model.struct.DBSTypedObject;
*/
public interface DBDArray extends DBDComplexType {
DBSDataType getElementType();
Object[] getContents()
throws DBCException;
......
package org.jkiss.dbeaver.model.data;
import org.jkiss.dbeaver.model.DBPDataSource;
import org.jkiss.dbeaver.model.struct.DBSDataType;
/**
* Complex type
*/
public interface DBDComplexType extends DBDValue {
DBSDataType getObjectDataType();
}
package org.jkiss.dbeaver.model.data;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.exec.DBCExecutionContext;
import org.jkiss.dbeaver.model.struct.DBSAttributeBase;
import org.jkiss.dbeaver.model.struct.DBSDataType;
import java.util.Collection;
/**
* Reference to another object (usually DBDStructure).
*/
public interface DBDReference extends DBDComplexType {
/**
* Retrieves referenced object.
* Object is retrieved in lazy way because references may point to owner objects in circular way.
* @return referenced object
* @throws DBCException
*/
Object getReferencedObject(DBCExecutionContext context)
throws DBCException;
}
......@@ -15,8 +15,6 @@ import java.util.Collection;
*/
public interface DBDStructure extends DBDComplexType {
DBSDataType getStructType();
Collection<DBSAttributeBase> getAttributes();
Object getAttributeValue(DBSAttributeBase attribute)
......
......@@ -72,13 +72,17 @@ public class JDBCArray implements DBDArray, DBDValueCloneable {
Object[] contents = null;
try {
contents = extractDataFromArray(context, array, type, valueHandler);
} catch (Exception e) {
try {
contents = extractDataFromResultSet(context, array, type, valueHandler);
} catch (Exception e1) {
log.warn("Could not extract array data from JDBC array"); //$NON-NLS-1$
contents = extractDataFromArray(context, array, type, valueHandler);
} catch (SQLException e) {
try {
contents = extractDataFromResultSet(context, array, type, valueHandler);
} catch (SQLException e1) {
throw new DBCException("Error reading from array result set", e1); //$NON-NLS-1$
}
}
} catch (DBCException e) {
log.warn("Can't extract array data from JDBC array", e); //$NON-NLS-1$
}
return new JDBCArray(type, contents);
}
......@@ -91,11 +95,16 @@ public class JDBCArray implements DBDArray, DBDValueCloneable {
}
try {
DBCResultSet resultSet = JDBCResultSetImpl.makeResultSet(context, dbResult, CoreMessages.model_jdbc_array_result_set);
List<Object> data = new ArrayList<Object>();
while (dbResult.next()) {
data.add(valueHandler.fetchValueObject(context, resultSet, type, 0));
try {
List<Object> data = new ArrayList<Object>();
while (dbResult.next()) {
// Fetch second column - it contains value
data.add(valueHandler.fetchValueObject(context, resultSet, type, 1));
}
return data.toArray();
} finally {
resultSet.close();
}
return data.toArray();
}
finally {
try {
......@@ -129,7 +138,7 @@ public class JDBCArray implements DBDArray, DBDValueCloneable {
}
@Override
public DBSDataType getElementType()
public DBSDataType getObjectDataType()
{
return type;
}
......
......@@ -154,14 +154,13 @@ public class JDBCArrayValueHandler extends JDBCAbstractValueHandler {
@Override
public void refreshValue()
{
editor.setModel((DBDArray) valueController.getValue());
editor.setModel(valueController.getDataSource(), (DBDArray) valueController.getValue());
}
@Override
protected Tree createControl(Composite editPlaceholder)
{
editor = new ComplexObjectEditor(valueController.getEditPlaceholder(), SWT.BORDER);
editor.setModel((DBDArray) valueController.getValue());
return editor.getTree();
}
......
/*
* Copyright (C) 2010-2012 Serge Rieder
* serge@jkiss.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.jkiss.dbeaver.model.impl.jdbc.data;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.data.DBDReference;
import org.jkiss.dbeaver.model.data.DBDValueHandler;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.exec.DBCExecutionContext;
import org.jkiss.dbeaver.model.struct.DBSDataType;
import java.sql.Ref;
import java.sql.SQLException;
/**
* Reference holder
*/
public class JDBCReference implements DBDReference {
static final Log log = LogFactory.getLog(JDBCReference.class);
private DBSDataType type;
private Ref value;
private JDBCReference()
{
}
public JDBCReference(DBSDataType type, Ref value) throws DBCException
{
this.type = type;
this.value = value;
}
public Ref getValue() throws DBCException
{
return value;
}
@Override
public boolean isNull()
{
return value == null;
}
@Override
public void release()
{
type = null;
value = null;
}
@Override
public DBSDataType getObjectDataType()
{
return type;
}
@Override
public Object getReferencedObject(DBCExecutionContext context) throws DBCException
{
try {
Object refValue = value.getObject();
DBDValueHandler valueHandler = DBUtils.findValueHandler(context, type);
return valueHandler.getValueFromObject(context, type, refValue, false);
} catch (SQLException e) {
throw new DBCException("Can't obtain object reference");
}
}
@Override
public String toString()
{
try {
return "REF on " + value.getBaseTypeName();
} catch (SQLException e) {
return value == null ? super.toString() : value.toString();
}
}
}
/*
* Copyright (C) 2010-2012 Serge Rieder
* serge@jkiss.org
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package org.jkiss.dbeaver.model.impl.jdbc.data;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.core.CoreMessages;
import org.jkiss.dbeaver.model.DBConstants;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.data.DBDDisplayFormat;
import org.jkiss.dbeaver.model.data.DBDStructure;
import org.jkiss.dbeaver.model.data.DBDValueController;
import org.jkiss.dbeaver.model.data.DBDValueEditor;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.exec.DBCExecutionContext;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCExecutionContext;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCPreparedStatement;
import org.jkiss.dbeaver.model.exec.jdbc.JDBCResultSet;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.struct.DBSDataType;
import org.jkiss.dbeaver.model.struct.DBSTypedObject;
import org.jkiss.dbeaver.ui.dialogs.data.ComplexObjectEditor;
import org.jkiss.dbeaver.ui.dialogs.data.DefaultValueViewDialog;
import org.jkiss.dbeaver.ui.properties.PropertySourceAbstract;
import java.sql.Ref;
import java.sql.SQLException;
/**
* JDBC reference value handler.
* Handle STRUCT types.
*
* @author Serge Rider
*/
public class JDBCReferenceValueHandler extends JDBCAbstractValueHandler {
static final Log log = LogFactory.getLog(JDBCReferenceValueHandler.class);
public static final JDBCReferenceValueHandler INSTANCE = new JDBCReferenceValueHandler();
@Override
public int getFeatures()
{
return FEATURE_VIEWER | FEATURE_EDITOR | FEATURE_SHOW_ICON;
}
/**
* NumberFormat is not thread safe thus this method is synchronized.
*/
@Override
public synchronized String getValueDisplayString(DBSTypedObject column, Object value, DBDDisplayFormat format)
{
JDBCReference reference = (JDBCReference) value;
return reference == null || reference.isNull() ? DBConstants.NULL_VALUE_LABEL : reference.toString();
}
@Override
protected Object fetchColumnValue(
DBCExecutionContext context,
JDBCResultSet resultSet,
DBSTypedObject type,
int index)
throws DBCException, SQLException
{
Ref value = resultSet.getRef(index);
return getValueFromObject(context, type, value, false);
}
@Override
protected void bindParameter(
JDBCExecutionContext context,
JDBCPreparedStatement statement,
DBSTypedObject paramType,
int paramIndex,
Object value)
throws DBCException, SQLException
{
JDBCReference reference = (JDBCReference) value;
statement.setRef(paramIndex, reference.getValue());
}
@Override
public Class getValueObjectType()
{
return Ref.class;
}
@Override
public JDBCReference getValueFromObject(DBCExecutionContext context, DBSTypedObject type, Object object, boolean copy) throws DBCException
{
String typeName;
try {
if (object instanceof Ref) {
typeName = ((Ref) object).getBaseTypeName();
} else {
typeName = type.getTypeName();
}
} catch (SQLException e) {
throw new DBCException(e);
}
DBSDataType dataType = null;
try {
dataType = DBUtils.resolveDataType(context.getProgressMonitor(), context.getDataSource(), typeName);
} catch (DBException e) {
log.error("Error resolving data type '" + typeName + "'", e);
}
if (object == null) {
return new JDBCReference(dataType, null);
} else if (object instanceof JDBCReference) {
return (JDBCReference)object;
} else if (object instanceof Ref) {
return new JDBCReference(dataType, (Ref) object);
} else {
throw new DBCException("Unsupported struct type: " + object.getClass().getName());
}
}
@Override
public DBDValueEditor createEditor(final DBDValueController controller)
throws DBException
{
return null;
}
}
\ No newline at end of file
......@@ -63,6 +63,8 @@ public class JDBCStandardValueHandlerProvider implements DBDValueHandlerProvider
return JDBCArrayValueHandler.INSTANCE;
case STRUCT:
return JDBCStructValueHandler.INSTANCE;
case REFERENCE:
return JDBCReferenceValueHandler.INSTANCE;
default:
return null;
}
......
......@@ -155,7 +155,7 @@ public class JDBCStruct implements DBDStructure, DBDValueCloneable {
}
@Override
public DBSDataType getStructType()
public DBSDataType getObjectDataType()
{
return type;
}
......
......@@ -182,14 +182,14 @@ public class JDBCStructValueHandler extends JDBCAbstractValueHandler {
@Override
public void refreshValue()
{
editor.setModel((DBDStructure) controller.getValue());
editor.setModel(controller.getDataSource(), (DBDStructure) controller.getValue());
}
@Override
protected Tree createControl(Composite editPlaceholder)
{
editor = new ComplexObjectEditor(controller.getEditPlaceholder(), SWT.BORDER);
editor.setModel((DBDStructure) controller.getValue());
editor.setModel(controller.getDataSource(), (DBDStructure) controller.getValue());
return editor.getTree();
}
......
......@@ -220,6 +220,9 @@ public class JDBCDataType implements DBSDataType
case Types.ROWID:
// threat ROWID as string
return DBSDataKind.STRING;
case Types.REF:
return DBSDataKind.REFERENCE;
}
return DBSDataKind.UNKNOWN;
}
......
......@@ -33,5 +33,6 @@ public enum DBSDataKind
STRUCT,
ARRAY,
OBJECT,
REFERENCE,
UNKNOWN,
}
......@@ -29,13 +29,20 @@ import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Tree;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.core.CoreMessages;
import org.jkiss.dbeaver.core.DBeaverUI;
import org.jkiss.dbeaver.model.DBPDataSource;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.data.*;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.exec.DBCExecutionContext;
import org.jkiss.dbeaver.model.exec.DBCExecutionPurpose;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.DBRRunnableWithResult;
import org.jkiss.dbeaver.model.struct.DBSAttributeBase;
import org.jkiss.dbeaver.model.struct.DBSTypedObject;
import org.jkiss.dbeaver.ui.UIUtils;
import java.lang.reflect.InvocationTargetException;
import java.util.Collection;
/**
......@@ -45,6 +52,8 @@ public class ComplexObjectEditor extends TreeViewer {
static final Log log = LogFactory.getLog(ComplexObjectEditor.class);
private DBPDataSource dataSource;
public ComplexObjectEditor(Composite parent, int style)
{
super(parent, style | SWT.SINGLE | SWT.FULL_SELECTION);
......@@ -62,6 +71,9 @@ public class ComplexObjectEditor extends TreeViewer {
if (!packing) {
packing = true;
UIUtils.packColumns(treeControl, true, new float[]{0.2f, 0.8f});
if (treeControl.getColumn(0).getWidth() < 100) {
treeControl.getColumn(0).setWidth(100);
}
treeControl.removeControlListener(this);
}
}
......@@ -85,12 +97,13 @@ public class ComplexObjectEditor extends TreeViewer {
super.setContentProvider(new StructContentProvider());
}
public void setModel(final DBDComplexType value)
public void setModel(DBPDataSource dataSource, final DBDComplexType value)
{
getTree().setRedraw(false);
try {
this.dataSource = dataSource;
setInput(value);
expandAll();
expandToLevel(2);
} finally {
getTree().setRedraw(true);
}
......@@ -120,7 +133,7 @@ public class ComplexObjectEditor extends TreeViewer {
this.array = array;
this.index = index;
this.value = value;
this.valueHandler = DBUtils.findValueHandler(array.getElementType().getDataSource(), array.getElementType());
this.valueHandler = DBUtils.findValueHandler(array.getObjectDataType().getDataSource(), array.getObjectDataType());
}
}
......@@ -163,7 +176,7 @@ public class ComplexObjectEditor extends TreeViewer {
int index = 0;
for (DBSAttributeBase attr : attributes) {
Object value = structure.getAttributeValue(attr);
children[index++] = new FieldInfo(structure.getStructType().getDataSource(), attr, value);
children[index++] = new FieldInfo(structure.getObjectDataType().getDataSource(), attr, value);
}
return children;
} catch (DBException e) {
......@@ -181,9 +194,38 @@ public class ComplexObjectEditor extends TreeViewer {
} catch (DBCException e) {
log.error("Error getting array content", e);
}
} else if (parent instanceof DBDReference) {
final DBDReference reference = (DBDReference)parent;
DBRRunnableWithResult<Object> runnable = new DBRRunnableWithResult<Object>() {
@Override
public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException
{
DBCExecutionContext context = dataSource.openContext(monitor, DBCExecutionPurpose.UTIL, "Read reference value");
try {
result = reference.getReferencedObject(context);
} catch (DBCException e) {
throw new InvocationTargetException(e);
} finally {
context.close();
}
}
};
try {
DBeaverUI.runInProgressService(runnable);
} catch (InvocationTargetException e) {
UIUtils.showErrorDialog(getControl().getShell(), "Value read", "Error read reference", e.getTargetException());
} catch (InterruptedException e) {
// ok
}
return getChildren(runnable.getResult());
} else if (parent instanceof FieldInfo) {
Object value = ((FieldInfo) parent).value;
if (value instanceof DBDStructure || value instanceof DBDArray) {
if (value instanceof DBDComplexType) {
return getChildren(value);
}
} else if (parent instanceof ArrayItem) {
Object value = ((ArrayItem) parent).value;
if (value instanceof DBDComplexType) {
return getChildren(value);
}
}
......@@ -193,7 +235,12 @@ public class ComplexObjectEditor extends TreeViewer {
@Override
public boolean hasChildren(Object parent)
{
return getChildren(parent).length > 0;
return
parent instanceof DBDStructure ||
parent instanceof DBDArray ||
parent instanceof DBDReference ||
(parent instanceof FieldInfo && hasChildren(((FieldInfo) parent).value)) ||
(parent instanceof ArrayItem && hasChildren(((ArrayItem) parent).value));
}
}
......@@ -212,20 +259,34 @@ public class ComplexObjectEditor extends TreeViewer {
if (isName) {
return field.attribute.getName();
}
if (field.value instanceof DBDStructure) {
return "";
}
return field.valueHandler.getValueDisplayString(field.attribute, field.value, DBDDisplayFormat.UI);
return getValueText(field.valueHandler, field.attribute, field.value);
} else if (obj instanceof ArrayItem) {
ArrayItem item = (ArrayItem) obj;
if (isName) {
return String.valueOf(item.index);
}
return item.valueHandler.getValueDisplayString(item.array.getElementType(), item.value, DBDDisplayFormat.UI);
return getValueText(item.valueHandler, item.array.getObjectDataType(), item.value);
}
return String.valueOf(columnIndex);
}
private String getValueText(DBDValueHandler valueHandler, DBSTypedObject type, Object value)
{
if (value instanceof DBDArray) {
try {
Object[] contents = ((DBDArray) value).getContents();
return "[" + ((DBDArray) value).getObjectDataType().getName() + " - " + String.valueOf(contents == null ? 0 : contents.length) + "]";
} catch (DBCException e) {
log.error(e);
return "N/A";
}
}
if (value instanceof DBDComplexType) {
return "[" + ((DBDComplexType) value).getObjectDataType().getName() + "]";
}
return valueHandler.getValueDisplayString(type, value, DBDDisplayFormat.UI);
}
@Override
public String getToolTipText(Object obj)
{
......
......@@ -22,6 +22,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.core.DBeaverUI;
import org.jkiss.dbeaver.model.DBPHiddenObject;
import org.jkiss.dbeaver.model.DBPNamedObject;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.DBRRunnableWithProgress;
......@@ -114,6 +115,10 @@ public class DiagramObjectCollector {
{
Collection<DBSEntity> tables = collectTables(monitor, roots);
for (DBSEntity table : tables) {
if (table instanceof DBPHiddenObject && ((DBPHiddenObject) table).isHidden()) {
// Skip hidden tables
continue;
}
addDiagramEntity(monitor, table);
}
......
......@@ -23,6 +23,7 @@ package org.jkiss.dbeaver.ext.erd.model;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.ext.erd.editor.ERDAttributeVisibility;
import org.jkiss.dbeaver.model.DBPHiddenObject;
import org.jkiss.dbeaver.model.DBUtils;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.struct.*;
......@@ -257,6 +258,10 @@ public class ERDEntity extends ERDObject<DBSEntity>
// usual thing in some systems like WMI/CIM model
continue;
}
if (attribute instanceof DBPHiddenObject && ((DBPHiddenObject) attribute).isHidden()) {
// Skip hidden attributes
continue;
}
switch (attributeVisibility) {
case PRIMARY:
if (!idColumns.contains(attribute)) {
......@@ -264,7 +269,7 @@ public class ERDEntity extends ERDObject<DBSEntity>
}
break;
case KEYS:
if (!keyColumns.contains(attribute)) {
if (keyColumns == null || !keyColumns.contains(attribute)) {
continue;
}
break;
......
......@@ -14,5 +14,5 @@ Require-Bundle: org.eclipse.ui,
Bundle-ActivationPolicy: lazy
Bundle-RequiredExecutionEnvironment: JavaSE-1.6
Bundle-Vendor: Serge Rieder
Bundle-ClassPath: .,external:$DRIVERS_LOCATION$/oracle/ojdbc6.jar
Bundle-ClassPath: .
Bundle-Localization: plugin
......@@ -25,6 +25,9 @@ import org.jkiss.dbeaver.model.exec.jdbc.JDBCResultSet;
import org.jkiss.dbeaver.model.impl.jdbc.data.JDBCContentValueHandler;
import org.jkiss.dbeaver.model.struct.DBSTypedObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLXML;
......@@ -44,6 +47,26 @@ public class OracleXMLValueHandler extends JDBCContentValueHandler {
object = resultSet.getObject(index);
} catch (SQLException e) {
object = resultSet.getSQLXML(index);
/*
try {
object = resultSet.getSQLXML(index);
} catch (SQLException e1) {
try {
ResultSet originalRS = resultSet.getOriginal();
Class<?> rsClass = originalRS.getClass().getClassLoader().loadClass("oracle.jdbc.OracleResultSet");
Method method = rsClass.getMethod("getOPAQUE", Integer.TYPE);
object = method.invoke(originalRS, index);
if (object != null) {
Class<?> xmlType = object.getClass().getClassLoader().loadClass("oracle.xdb.XMLType");
Method xmlConstructor = xmlType.getMethod("createXML", object.getClass());
object = xmlConstructor.invoke(null, object);
}
}
catch (Throwable e2) {
object = null;
}
}
*/
}
if (object == null) {
......
......@@ -26,6 +26,7 @@ import org.jkiss.dbeaver.model.struct.DBSDataType;
import org.jkiss.dbeaver.model.struct.DBSEntityAttribute;
import java.sql.ResultSet;
import java.sql.Types;
/**
* Oracle data type attribute
......@@ -96,6 +97,10 @@ public class OracleDataTypeAttribute extends OracleDataTypeMember implements DBS
@Override
public int getTypeID()
{
if (attrTypeMod == OracleDataTypeModifier.REF) {
// Explicitly say that we are reference
return Types.REF;
}
return attrType.getTypeID();
}
......
......@@ -33,6 +33,7 @@ import org.jkiss.dbeaver.ui.DBIcon;
import org.jkiss.dbeaver.ui.properties.IPropertyValueListProvider;
import java.sql.ResultSet;
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
......@@ -74,6 +75,9 @@ public class OracleTableColumn extends JDBCTableColumn<OracleTableBase> implemen
this.typeName = type.getFullQualifiedName();
this.valueType = type.getTypeID();
}
if (typeMod == OracleDataTypeModifier.REF) {
this.valueType = Types.REF;
}
setMaxLength(JDBCUtils.safeGetLong(dbResult, "DATA_LENGTH"));
setRequired(!"Y".equals(JDBCUtils.safeGetString(dbResult, "NULLABLE")));
setScale(JDBCUtils.safeGetInt(dbResult, "DATA_SCALE"));
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册