/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2017 Serge Rider (serge@jkiss.org)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jkiss.dbeaver.model.navigator;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.runtime.Status;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.messages.ModelMessages;
import org.jkiss.dbeaver.model.*;
import org.jkiss.dbeaver.model.exec.DBCExecutionContext;
import org.jkiss.dbeaver.model.navigator.meta.DBXTreeFolder;
import org.jkiss.dbeaver.model.navigator.meta.DBXTreeItem;
import org.jkiss.dbeaver.model.navigator.meta.DBXTreeNode;
import org.jkiss.dbeaver.model.navigator.meta.DBXTreeObject;
import org.jkiss.dbeaver.model.runtime.DBRProgressListener;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
import org.jkiss.dbeaver.model.struct.*;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.BeanUtils;
import org.jkiss.utils.CommonUtils;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.*;
/**
* DBNDatabaseNode
*/
public abstract class DBNDatabaseNode extends DBNNode implements DBSWrapper, DBPContextProvider, IDataSourceContainerProvider {
private static final DBNDatabaseNode[] EMPTY_NODES = new DBNDatabaseNode[0];
private volatile boolean locked;
protected volatile DBNDatabaseNode[] childNodes;
private boolean filtered;
protected DBNDatabaseNode(DBNNode parentNode)
{
super(parentNode);
}
protected void registerNode() {
DBNModel model = getModel();
if (model != null) {
model.addNode(this);
}
}
protected void unregisterNode(boolean reflect) {
DBNModel model = getModel();
if (model != null) {
model.removeNode(this, reflect);
}
}
@Override
void dispose(boolean reflect)
{
clearChildren(reflect);
super.dispose(reflect);
}
@Override
public String getNodeType()
{
return getObject() == null ? "" : getMeta().getNodeType(getObject().getDataSource()); //$NON-NLS-1$
}
@Override
public String getNodeName()
{
DBSObject object = getObject();
if (object == null) {
return DBConstants.NULL_VALUE_LABEL;
}
String objectName;
if (object instanceof DBPOverloadedObject) {
objectName = ((DBPOverloadedObject) object).getOverloadedName();
} else {
objectName = object.getName();
}
if (CommonUtils.isEmpty(objectName)) {
objectName = object.toString();
if (CommonUtils.isEmpty(objectName)) {
objectName = object.getClass().getName() + "@" + object.hashCode(); //$NON-NLS-1$
}
}
/*
if (object instanceof DBPUniqueObject) {
String uniqueName = ((DBPUniqueObject) object).getUniqueName();
if (!uniqueName.equals(objectName)) {
if (uniqueName.startsWith(objectName)) {
uniqueName = uniqueName.substring(objectName.length());
}
objectName += " (" + uniqueName + ")";
}
}
*/
return objectName;
}
@Override
public String getNodeFullName()
{
if (getObject() instanceof DBPQualifiedObject) {
return ((DBPQualifiedObject)getObject()).getFullyQualifiedName(DBPEvaluationContext.UI);
} else {
return super.getNodeFullName();
}
}
@Override
public String getNodeDescription()
{
return getObject() == null ? null : getObject().getDescription();
}
@Override
public DBPImage getNodeIcon()
{
final DBSObject object = getObject();
DBPImage image = DBValueFormatting.getObjectImage(object, false);
if (image == null) {
DBXTreeNode meta = getMeta();
if (meta != null) {
image = meta.getIcon(this);
}
}
if (image != null && object instanceof DBPStatefulObject) {
image = DBNModel.getStateOverlayImage(image, ((DBPStatefulObject) object).getObjectState());
}
return image;
}
@Override
public boolean allowsChildren()
{
return !isDisposed() && this.getMeta().hasChildren(this);
}
@Override
public boolean allowsNavigableChildren()
{
return !isDisposed() && this.getMeta().hasChildren(this, true);
}
public boolean hasChildren(DBRProgressMonitor monitor, DBXTreeNode childType)
throws DBException
{
if (isDisposed()) {
return false;
}
DBNDatabaseNode[] children = getChildren(monitor);
if (!ArrayUtils.isEmpty(children)) {
for (DBNDatabaseNode child : children) {
if (child.getMeta() == childType) {
return true;
}
}
}
return false;
}
@Override
public synchronized DBNDatabaseNode[] getChildren(DBRProgressMonitor monitor)
throws DBException
{
if (childNodes == null && hasChildren(false)) {
if (this.initializeNode(monitor, null)) {
final List<DBNDatabaseNode> tmpList = new ArrayList<>();
loadChildren(monitor, getMeta(), null, tmpList, true);
if (!monitor.isCanceled()) {
if (tmpList.isEmpty()) {
this.childNodes = EMPTY_NODES;
} else {
this.childNodes = tmpList.toArray(new DBNDatabaseNode[tmpList.size()]);
}
this.afterChildRead();
}
}
}
return childNodes;
}
protected void afterChildRead()
{
// Do nothing
}
DBNDatabaseNode[] getChildNodes()
{
return childNodes;
}
boolean hasChildItem(DBSObject object)
{
if (childNodes != null) {
for (DBNDatabaseNode child : childNodes) {
if (child.getObject() == object) {
return true;
}
}
}
return false;
}
void addChildItem(DBSObject object)
{
DBXTreeItem metaChildren = getItemsMeta();
if (metaChildren != null) {
final DBNDatabaseItem newChild = new DBNDatabaseItem(this, metaChildren, object, false);
synchronized (this) {
childNodes = ArrayUtils.add(DBNDatabaseNode.class, childNodes, newChild);
}
getModel().fireNodeEvent(new DBNEvent(this, DBNEvent.Action.ADD, DBNEvent.NodeChange.LOAD, newChild));
} else {
log.error("Cannot add child item to " + getNodeName() + ". Conditions doesn't met"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
void removeChildItem(DBSObject object)
{
DBNNode childNode = null;
synchronized (this) {
if (!ArrayUtils.isEmpty(childNodes)) {
for (int i = 0; i < childNodes.length; i++) {
final DBNDatabaseNode child = childNodes[i];
if (child.getObject() == object) {
childNode = child;
childNodes = ArrayUtils.remove(DBNDatabaseNode.class, childNodes, i);
break;
}
}
}
}
if (childNode != null) {
childNode.dispose(true);
}
}
@Override
void clearNode(boolean reflect) {
clearChildren(reflect);
}
/**
* Reorder children nodes
*/
public void updateChildrenOrder(boolean reflect) {
try {
refreshNodeContent(new VoidProgressMonitor(), getObject(), this, reflect);
} catch (DBException e) {
log.error("Error reordering node children", e);
}
}
public boolean needsInitialization()
{
return childNodes == null && hasChildren(false);
}
@Override
public boolean isLocked()
{
return locked || super.isLocked();
}
public boolean initializeNode(DBRProgressMonitor monitor, DBRProgressListener onFinish)
{
if (onFinish != null) {
onFinish.onTaskFinished(Status.OK_STATUS);
}
return true;
}
/**
* Refreshes node.
* If refresh cannot be done in this level then refreshes parent node.
* Do not actually changes navigation tree. If some underlying object is refreshed it must fire DB model
* event which will cause actual tree nodes refresh. Underlying object could present multiple times in
* navigation model - each occurrence will be refreshed then.
*
* @param monitor progress monitor
* @param source source object
* @return real refreshed node or null if nothing was refreshed
* @throws DBException on any internal exception
*/
@Override
public DBNNode refreshNode(DBRProgressMonitor monitor, Object source) throws DBException
{
if (isLocked()) {
log.warn("Attempt to refresh locked node '" + getNodeName() + "'"); //$NON-NLS-1$ //$NON-NLS-2$
return null;
}
DBSObject object = getObject();
if (object instanceof DBPRefreshableObject) {
if (object.isPersisted()) {
DBSObject newObject = ((DBPRefreshableObject) object).refreshObject(monitor);
if (newObject == null) {
if (parentNode instanceof DBNDatabaseNode) {
((DBNDatabaseNode) parentNode).removeChildItem(object);
}
return null;
} else {
refreshNodeContent(monitor, newObject, source, true);
return this;
}
} else {
// Not persisted node - nothing to refresh
getModel().fireNodeUpdate(source, this, DBNEvent.NodeChange.REFRESH);
return this;
}
} else {
return super.refreshNode(monitor, source);
}
}
private void refreshNodeContent(final DBRProgressMonitor monitor, DBSObject newObject, Object source, boolean reflect)
throws DBException
{
if (isDisposed()) {
return;
}
this.locked = true;
DBNModel model = getModel();
try {
if (newObject != getObject()) {
reloadObject(monitor, newObject);
}
if (reflect) model.fireNodeUpdate(source, this, DBNEvent.NodeChange.LOCK);
this.reloadChildren(monitor, reflect);
if (reflect) model.fireNodeUpdate(source, this, DBNEvent.NodeChange.REFRESH);
} finally {
this.locked = false;
// Unlock node
if (reflect) model.fireNodeUpdate(source, this, DBNEvent.NodeChange.UNLOCK);
}
}
protected void clearChildren(boolean reflect)
{
DBNDatabaseNode[] childrenCopy;
synchronized (this) {
childrenCopy = childNodes == null ? null : Arrays.copyOf(childNodes, childNodes.length);
childNodes = null;
}
if (childrenCopy != null) {
for (DBNNode child : childrenCopy) {
child.dispose(reflect);
}
}
}
private void loadChildren(
DBRProgressMonitor monitor,
final DBXTreeNode meta,
final DBNDatabaseNode[] oldList,
final List<DBNDatabaseNode> toList,
boolean reflect)
throws DBException
{
if (monitor.isCanceled()) {
return;
}
this.filtered = false;
List<DBXTreeNode> childMetas = meta.getChildren(this);
if (CommonUtils.isEmpty(childMetas)) {
return;
}
monitor.beginTask(ModelMessages.model_navigator_load_items_, childMetas.size());
for (DBXTreeNode child : childMetas) {
if (monitor.isCanceled()) {
break;
}
monitor.subTask(ModelMessages.model_navigator_load_ + " " + child.getChildrenType(getObject().getDataSource()));
if (child instanceof DBXTreeItem) {
final DBXTreeItem item = (DBXTreeItem) child;
boolean isLoaded = loadTreeItems(monitor, item, oldList, toList, reflect);
if (!isLoaded && item.isOptional() && item.getRecursiveLink() == null) {
// This may occur only if no child nodes was read
// Then we try to go on next DBX level
loadChildren(monitor, item, oldList, toList, reflect);
}
} else if (child instanceof DBXTreeFolder) {
if (oldList == null) {
// Load new folders only if there are no old ones
toList.add(
new DBNDatabaseFolder(this, (DBXTreeFolder) child));
} else {
for (DBNDatabaseNode oldFolder : oldList) {
if (oldFolder.getMeta() == child) {
oldFolder.reloadChildren(monitor, reflect);
toList.add(oldFolder);
break;
}
}
}
} else if (child instanceof DBXTreeObject) {
if (oldList == null) {
// Load new objects only if there are no old ones
toList.add(
new DBNDatabaseObject(this, (DBXTreeObject) child));
} else {
for (DBNDatabaseNode oldObject : oldList) {
if (oldObject.getMeta() == child) {
oldObject.reloadChildren(monitor, reflect);
toList.add(oldObject);
break;
}
}
}
} else {
log.warn("Unsupported meta node type: " + child); //$NON-NLS-1$
}
monitor.worked(1);
}
monitor.done();
if (reflect && filtered) {
getModel().fireNodeUpdate(this, this, DBNEvent.NodeChange.REFRESH);
}
}
/**
* Extract items using reflect api
* @param monitor progress monitor
* @param meta items meta info
* @param oldList previous child items
* @param toList list ot add new items @return true on success
* @param reflect
* @return true on success
* @throws DBException on any DB error
*/
private boolean loadTreeItems(
DBRProgressMonitor monitor,
DBXTreeItem meta,
final DBNDatabaseNode[] oldList,
final List<DBNDatabaseNode> toList,
boolean reflect)
throws DBException
{
if (this.isDisposed()) {
// Property reading can take really long time so this node can be disposed at this moment -
// check it
return false;
}
// Read property using reflection
Object valueObject = getValueObject();
if (valueObject == null) {
return false;
}
String propertyName = meta.getPropertyName();
Object propertyValue = extractPropertyValue(monitor, valueObject, propertyName);
if (propertyValue == null) {
return false;
}
if (!(propertyValue instanceof Collection<?>)) {
log.warn("Bad property '" + propertyName + "' value: " + propertyValue.getClass().getName()); //$NON-NLS-1$ //$NON-NLS-2$
return false;
}
DBSObjectFilter filter = getNodeFilter(meta, false);
this.filtered = filter != null && !filter.isNotApplicable();
Collection<?> itemList = (Collection<?>) propertyValue;
if (itemList.isEmpty()) {
return false;
}
if (this.isDisposed()) {
// Property reading can take really long time so this node can be disposed at this moment -
// check it
return false;
}
DBPDataSourceContainer dataSourceContainer = getDataSourceContainer();
boolean showSystem = dataSourceContainer == null || dataSourceContainer.isShowSystemObjects();
for (Object childItem : itemList) {
if (childItem == null) {
continue;
}
if (!(childItem instanceof DBSObject)) {
log.warn("Bad item type: " + childItem.getClass().getName()); //$NON-NLS-1$
continue;
}
if (DBUtils.isHiddenObject(childItem)) {
// Skip hidden objects
continue;
}
if (!showSystem && childItem instanceof DBPSystemObject && ((DBPSystemObject) childItem).isSystem()) {
// Skip system objects
continue;
}
if (filter != null && !filter.matches(((DBSObject)childItem).getName())) {
// Doesn't match filter
continue;
}
DBSObject object = (DBSObject)childItem;
boolean added = false;
if (oldList != null) {
// Check that new object is a replacement of old one
for (DBNDatabaseNode oldChild : oldList) {
if (oldChild.getMeta() == meta && equalObjects(oldChild.getObject(), object)) {
oldChild.reloadObject(monitor, object);
if (oldChild.hasChildren(false) && !oldChild.needsInitialization()) {
// Refresh children recursive
oldChild.reloadChildren(monitor, reflect);
}
if (reflect) {
getModel().fireNodeUpdate(this, oldChild, DBNEvent.NodeChange.REFRESH);
}
toList.add(oldChild);
added = true;
break;
}
}
}
if (!added) {
// Simply add new item
DBNDatabaseItem treeItem = new DBNDatabaseItem(this, meta, object, oldList != null);
toList.add(treeItem);
}
}
if (oldList != null) {
// Now remove all non-existing items
for (DBNDatabaseNode oldChild : oldList) {
if (oldChild.getMeta() != meta) {
// Wrong type
continue;
}
boolean found = false;
for (Object childItem : itemList) {
if (childItem instanceof DBSObject && equalObjects(oldChild.getObject(), (DBSObject) childItem)) {
found = true;
break;
}
}
if (!found) {
// Remove old child object
oldChild.dispose(true);
}
}
}
return true;
}
@Nullable
@Override
public DBCExecutionContext getExecutionContext() {
DBPDataSource dataSource = getDataSource();
return dataSource == null ? null : dataSource.getDefaultContext(true);
}
@NotNull
public DBPDataSourceContainer getDataSourceContainer()
{
for (DBNNode p = getParentNode(); p != null; p = p.getParentNode()) {
if (p instanceof DBNDataSource) {
return ((DBNDataSource) p).getDataSourceContainer();
}
}
throw new IllegalStateException("No parent datasource node");
}
public DBPDataSource getDataSource() {
DBSObject object = getObject();
return object == null ? null : object.getDataSource();
}
public DBSObjectFilter getNodeFilter(DBXTreeItem meta, boolean firstMatch)
{
DBPDataSourceContainer dataSource = getDataSourceContainer();
if (dataSource != null && this instanceof DBNContainer) {
Class<?> childrenClass = this.getChildrenClass(meta);
if (childrenClass != null) {
Object valueObject = getValueObject();
DBSObject parentObject = null;
if (valueObject instanceof DBSObject && !(valueObject instanceof DBPDataSource)) {
parentObject = (DBSObject) valueObject;
}
return dataSource.getObjectFilter(childrenClass, parentObject, firstMatch);
}
}
return null;
}
public void setNodeFilter(DBXTreeItem meta, DBSObjectFilter filter)
{
DBPDataSourceContainer dataSource = getDataSourceContainer();
if (dataSource != null && this instanceof DBNContainer) {
Class<?> childrenClass = this.getChildrenClass(meta);
if (childrenClass != null) {
Object parentObject = getValueObject();
if (parentObject instanceof DBPDataSource) {
parentObject = null;
}
dataSource.setObjectFilter(
this.getChildrenClass(meta),
(DBSObject) parentObject,
filter);
dataSource.persistConfiguration();
}
} else {
log.error("No active datasource - can't save filter configuration");
}
}
@Override
public boolean isFiltered()
{
return filtered;
}
@Override
public String getNodeItemPath() {
StringBuilder pathName = new StringBuilder(100);
for (DBNNode node = this; node instanceof DBNDatabaseNode; node = node.getParentNode()) {
if (node instanceof DBNDataSource) {
if (pathName.length() > 0) {
pathName.insert(0, '/');
}
pathName.insert(0, ((DBNDataSource) node).getDataSourceContainer().getId());
} else if (node instanceof DBNDatabaseFolder) {
if (pathName.length() > 0) {
pathName.insert(0, '/');
}
String type = ((DBNDatabaseFolder) node).getMeta().getType();
if (CommonUtils.isEmpty(type)) {
type = node.getName();
}
pathName.insert(0, type);
}
if (!(node instanceof DBNDatabaseItem) && !(node instanceof DBNDatabaseObject)) {
// skip folders
continue;
}
if (pathName.length() > 0) {
pathName.insert(0, '/');
}
pathName.insert(0, node.getNodeName().replace('/', '_'));
}
pathName.insert(0, NodePathType.database.getPrefix());
return pathName.toString();
}
protected void reloadChildren(DBRProgressMonitor monitor, boolean reflect)
throws DBException
{
DBNDatabaseNode[] oldChildren;
synchronized (this) {
if (childNodes == null) {
// Nothing to reload
return;
}
oldChildren = Arrays.copyOf(childNodes, childNodes.length);
}
List<DBNDatabaseNode> newChildren = new ArrayList<>();
loadChildren(monitor, getMeta(), oldChildren, newChildren, reflect);
synchronized (this) {
childNodes = newChildren.toArray(new DBNDatabaseNode[newChildren.size()]);
}
}
private static boolean equalObjects(DBSObject object1, DBSObject object2) {
if (object1 == object2) {
return true;
}
if (object1 == null || object2 == null) {
return false;
}
while (object1 != null && object2 != null) {
if (object1.getClass() != object2.getClass() ||
!CommonUtils.equalObjects(DBUtils.getObjectUniqueName(object1), DBUtils.getObjectUniqueName(object2)))
{
return false;
}
object1 = object1.getParentObject();
object2 = object2.getParentObject();
}
return true;
}
public abstract Object getValueObject();
public abstract DBXTreeNode getMeta();
public DBXTreeItem getItemsMeta()
{
List<DBXTreeNode> metaChildren = getMeta().getChildren(this);
if (metaChildren != null && metaChildren.size() == 1 && metaChildren.get(0) instanceof DBXTreeItem) {
return (DBXTreeItem)metaChildren.get(0);
} else {
return null;
}
}
protected abstract void reloadObject(DBRProgressMonitor monitor, DBSObject object);
public List<Class<?>> getChildrenTypes(DBXTreeNode useMeta)
{
List<DBXTreeNode> childMetas = useMeta == null ? getMeta().getChildren(this) : Collections.singletonList(useMeta);
if (CommonUtils.isEmpty(childMetas)) {
return null;
} else {
List<Class<?>> result = new ArrayList<>();
for (DBXTreeNode childMeta : childMetas) {
if (childMeta instanceof DBXTreeItem) {
Class<?> childrenType = getChildrenClass((DBXTreeItem) childMeta);
if (childrenType != null) {
result.add(childrenType);
}
}
}
return result;
}
}
private Class<?> getChildrenClass(DBXTreeItem childMeta)
{
Object valueObject = getValueObject();
if (valueObject == null) {
return null;
}
String propertyName = childMeta.getPropertyName();
Method getter = findPropertyReadMethod(valueObject.getClass(), propertyName);
if (getter == null) {
return null;
}
Type propType = getter.getGenericReturnType();
return BeanUtils.getCollectionType(propType);
}
public IProject getOwnerProject() {
for (DBNNode node = getParentNode(); node != null; node = node.getParentNode()) {
if (node instanceof DBNProject) {
return ((DBNProject) node).getProject();
}
}
return null;
}
////////////////////////////////////////////////////////////////////////////////////
// Reflection utils
public static Object extractPropertyValue(DBRProgressMonitor monitor, Object object, String propertyName)
throws DBException
{
// Read property using reflection
if (object == null) {
return null;
}
try {
Method getter = findPropertyReadMethod(object.getClass(), propertyName);
if (getter == null) {
log.warn("Can't find property '" + propertyName + "' read method in '" + object.getClass().getName() + "'");
return null;
}
Class<?>[] paramTypes = getter.getParameterTypes();
if (paramTypes.length == 0) {
// No params - just read it
return getter.invoke(object);
} else if (paramTypes.length == 1 && paramTypes[0] == DBRProgressMonitor.class) {
// Read with progress monitor
return getter.invoke(object, monitor);
} else {
log.warn("Can't read property '" + propertyName + "' - bad method signature: " + getter.toString());
return null;
}
}
catch (IllegalAccessException ex) {
log.warn("Error accessing items " + propertyName, ex);
return null;
}
catch (InvocationTargetException ex) {
if (ex.getTargetException() instanceof DBException) {
throw (DBException) ex.getTargetException();
}
throw new DBException("Can't read " + propertyName, ex.getTargetException());
}
}
public static Method findPropertyReadMethod(Class<?> clazz, String propertyName)
{
String methodName = BeanUtils.propertyNameToMethodName(propertyName);
return findPropertyGetter(clazz, "get" + methodName, "is" + methodName);
}
private static Method findPropertyGetter(Class<?> clazz, String getName, String isName)
{
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (
(!Modifier.isPublic(method.getModifiers())) ||
(!Modifier.isPublic(method.getDeclaringClass().getModifiers())) ||
(method.getReturnType().equals(void.class)))
{
// skip
} else if (method.getName().equals(getName) || (method.getName().equals(isName) && method.getReturnType().equals(boolean.class))) {
// If it matches the get name, it's the right method
Class<?>[] parameterTypes = method.getParameterTypes();
if (parameterTypes.length == 0 || (parameterTypes.length == 1 && parameterTypes[0] == DBRProgressMonitor.class)) {
return method;
}
}
}
return clazz == Object.class ? null : findPropertyGetter(clazz.getSuperclass(), getName, isName);
}
}