/*
* 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.ui.editors.sql.syntax;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Display;
import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.*;
import org.jkiss.dbeaver.model.data.DBDDisplayFormat;
import org.jkiss.dbeaver.model.exec.DBCExecutionContext;
import org.jkiss.dbeaver.model.impl.DBObjectNameCaseTransformer;
import org.jkiss.dbeaver.model.impl.struct.DirectObjectReference;
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.DBRRunnableWithProgress;
import org.jkiss.dbeaver.model.runtime.SystemJob;
import org.jkiss.dbeaver.model.sql.SQLDialect;
import org.jkiss.dbeaver.model.sql.SQLHelpProvider;
import org.jkiss.dbeaver.model.sql.SQLHelpTopic;
import org.jkiss.dbeaver.model.sql.SQLSyntaxManager;
import org.jkiss.dbeaver.model.struct.*;
import org.jkiss.dbeaver.runtime.jobs.DataSourceJob;
import org.jkiss.dbeaver.runtime.properties.PropertyCollector;
import org.jkiss.dbeaver.ui.DBeaverIcons;
import org.jkiss.dbeaver.ui.UIIcon;
import org.jkiss.dbeaver.ui.UIUtils;
import org.jkiss.dbeaver.ui.editors.sql.SQLEditorBase;
import org.jkiss.utils.ArrayUtils;
import org.jkiss.utils.CommonUtils;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.*;
/**
* SQLContextInformer
*/
public class SQLContextInformer
{
private static final Log log = Log.getLog(SQLContextInformer.class);
private final SQLEditorBase editor;
private SQLSyntaxManager syntaxManager;
private SQLIdentifierDetector.WordRegion wordRegion;
private static class ObjectLookupCache {
List<DBSObjectReference> references;
boolean loading = true;
}
private static final Map<SQLEditorBase, Map<String, ObjectLookupCache>> linksCache = new HashMap<>();
private String[] keywords;
private DBPKeywordType keywordType;
private List<DBSObjectReference> objectReferences;
public SQLContextInformer(SQLEditorBase editor, SQLSyntaxManager syntaxManager)
{
this.editor = editor;
this.syntaxManager = syntaxManager;
}
public SQLEditorBase getEditor() {
return editor;
}
public SQLIdentifierDetector.WordRegion getWordRegion() {
return wordRegion;
}
public String[] getKeywords() {
return keywords;
}
public DBPKeywordType getKeywordType() {
return keywordType;
}
public synchronized List<DBSObjectReference> getObjectReferences() {
return objectReferences;
}
public synchronized boolean hasObjects() {
return !CommonUtils.isEmpty(objectReferences);
}
public void searchInformation(IRegion region)
{
initEditorCache();
ITextViewer textViewer = editor.getTextViewer();
final DBCExecutionContext executionContext = editor.getExecutionContext();
if (region == null || textViewer == null || executionContext == null) {
return;
}
IDocument document = textViewer.getDocument();
if (document == null) {
return;
}
SQLWordPartDetector wordDetector = new SQLWordPartDetector(document, syntaxManager, region.getOffset());
wordRegion = wordDetector.detectIdentifier(document, region);
if (wordRegion.word.length() == 0) {
return;
}
String fullName = wordRegion.identifier;
String tableName = wordRegion.word;
boolean caseSensitive = false;
if (wordDetector.isQuoted(tableName)) {
tableName = DBUtils.getUnQuotedIdentifier(tableName, syntaxManager.getQuoteSymbol());
caseSensitive = true;
}
String[] containerNames = null;
if (!CommonUtils.equalObjects(fullName, tableName)) {
int divPos = fullName.indexOf(syntaxManager.getStructSeparator());
if (divPos != -1) {
String[] parts = ArrayUtils.toArray(String.class, CommonUtils.splitString(fullName, syntaxManager.getStructSeparator()));
tableName = parts[parts.length - 1];
containerNames = ArrayUtils.remove(String.class, parts, parts.length - 1);
for (int i = 0; i < containerNames.length; i++) {
if (wordDetector.isQuoted(containerNames[i])) {
containerNames[i] = DBUtils.getUnQuotedIdentifier(containerNames[i], syntaxManager.getQuoteSymbol());
}
containerNames[i] = DBObjectNameCaseTransformer.transformName(editor.getDataSource(), containerNames[i]);
}
if (wordDetector.isQuoted(tableName)) {
tableName = DBUtils.getUnQuotedIdentifier(tableName, syntaxManager.getQuoteSymbol());
}
} else {
// Full name could be quoted
if (wordDetector.isQuoted(fullName)) {
String unquotedName = DBUtils.getUnQuotedIdentifier(tableName, syntaxManager.getQuoteSymbol());
if (unquotedName.equals(tableName)) {
caseSensitive = true;
}
}
}
}
final SQLDialect dialect = syntaxManager.getDialect();
keywordType = dialect.getKeywordType(fullName);
if (keywordType == DBPKeywordType.KEYWORD && region.getLength() > 1) {
// It is a keyword = let's use whole selection
try {
fullName = document.get(region.getOffset(), region.getLength());
} catch (BadLocationException e) {
log.warn(e);
}
}
keywords = new String[] { fullName };
if (keywordType == DBPKeywordType.KEYWORD || keywordType == DBPKeywordType.FUNCTION) {
// Skip keywords
return;
}
DBSStructureAssistant structureAssistant = DBUtils.getAdapter(DBSStructureAssistant.class, editor.getDataSource());
if (structureAssistant == null) {
return;
}
final Map<String, ObjectLookupCache> contextCache = getLinksCache();
if (contextCache == null) {
return;
}
ObjectLookupCache tlc = contextCache.get(fullName);
if (tlc == null) {
// Start new word finder job
tlc = new ObjectLookupCache();
contextCache.put(fullName, tlc);
TablesFinderJob job = new TablesFinderJob(executionContext, structureAssistant, containerNames, tableName, caseSensitive, tlc);
job.schedule();
}
if (tlc.loading) {
// Wait for 1000ms maximum
for (int i = 0; i < 20; i++) {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// interrupted - just go further
break;
}
if (!tlc.loading) {
break;
}
Display.getCurrent().readAndDispatch();
}
}
if (!tlc.loading) {
synchronized (this) {
objectReferences = tlc.references;
}
}
}
private void initEditorCache() {
// Register cache for specified editor
boolean dupEditor;
synchronized (linksCache) {
dupEditor = linksCache.containsKey(this.editor);
if (!dupEditor) {
linksCache.put(this.editor, new HashMap<String, ObjectLookupCache>());
}
}
if (!dupEditor) {
editor.getTextViewer().getControl().addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent e) {
synchronized (linksCache) {
linksCache.remove(SQLContextInformer.this.editor);
}
}
});
}
}
private Map<String, ObjectLookupCache> getLinksCache() {
synchronized (linksCache) {
return linksCache.get(this.editor);
}
}
public static String makeObjectDescription(@Nullable DBRProgressMonitor monitor, DBPNamedObject object, boolean html) {
final PropertiesReader reader = new PropertiesReader(object, html);
if (monitor == null) {
SystemJob searchJob = new SystemJob("Extract object properties info", reader);
searchJob.schedule();
UIUtils.waitJobCompletion(searchJob);
} else {
reader.run(monitor);
}
return reader.getPropertiesInfo();
}
public static String readAdditionalProposalInfo(@Nullable DBRProgressMonitor monitor, final DBPDataSource dataSource, DBPNamedObject object, final String[] keywords, final DBPKeywordType keywordType) {
if (object != null) {
return makeObjectDescription(monitor, object, true);
} else if (keywordType != null && dataSource != null) {
HelpReader helpReader = new HelpReader(dataSource, keywordType, keywords);
if (monitor == null) {
SystemJob searchJob = new SystemJob("Read help topic", helpReader);
searchJob.schedule();
UIUtils.waitJobCompletion(searchJob);
} else {
helpReader.run(monitor);
}
return helpReader.info;
} else {
return keywords.length == 0 ? null : keywords[0];
}
}
private static String readDataSourceHelp(DBRProgressMonitor monitor, DBPDataSource dataSource, DBPKeywordType keywordType, String keyword) {
final SQLHelpProvider helpProvider = DBUtils.getAdapter(SQLHelpProvider.class, dataSource);
if (helpProvider == null) {
return null;
}
final SQLHelpTopic helpTopic = helpProvider.findHelpTopic(monitor, keyword, keywordType);
if (helpTopic == null) {
return null;
}
if (!CommonUtils.isEmpty(helpTopic.getContents())) {
return helpTopic.getContents();
} else if (!CommonUtils.isEmpty(helpTopic.getUrl())) {
return "<a href=\"" + helpTopic.getUrl() + "\">" + keyword + "</a>";
} else {
return null;
}
}
private static class PropertiesReader implements DBRRunnableWithProgress {
private final StringBuilder info = new StringBuilder();
private final DBPNamedObject object;
private final boolean html;
public PropertiesReader(DBPNamedObject object, boolean html) {
this.object = object;
this.html = html;
}
/*
boolean hasRemoteProperties() {
for (DBPPropertyDescriptor descriptor : collector.getPropertyDescriptors2()) {
if (descriptor.isRemote()) {
return true;
}
}
return false;
}
*/
String getPropertiesInfo() {
return info.toString();
}
@Override
public void run(DBRProgressMonitor monitor) {
DBPNamedObject targetObject = object;
if (object instanceof DBSObjectReference) {
try {
targetObject = ((DBSObjectReference) object).resolveObject(monitor);
} catch (DBException e) {
StringWriter buf = new StringWriter();
e.printStackTrace(new PrintWriter(buf, true));
info.append(buf.toString());
}
}
PropertyCollector collector = new PropertyCollector(targetObject, false);
collector.collectProperties();
for (DBPPropertyDescriptor descriptor : collector.getPropertyDescriptors2()) {
Object propValue = collector.getPropertyValue(monitor, descriptor.getId());
if (propValue == null) {
continue;
}
String propString;
if (propValue instanceof DBPNamedObject) {
propString = ((DBPNamedObject) propValue).getName();
} else {
propString = DBValueFormatting.getDefaultValueDisplayString(propValue, DBDDisplayFormat.UI);
}
if (CommonUtils.isEmpty(propString)) {
continue;
}
if (html) {
info.append("<b>").append(descriptor.getDisplayName()).append(": </b>");
info.append(propString);
info.append("<br>");
} else {
info.append(descriptor.getDisplayName()).append(": ").append(propString).append("\n");
}
}
}
}
private static class HelpReader implements DBRRunnableWithProgress {
private final DBPDataSource dataSource;
private final DBPKeywordType keywordType;
private final String[] keywords;
private String info;
public HelpReader(DBPDataSource dataSource, DBPKeywordType keywordType, String[] keywords) {
this.dataSource = dataSource;
this.keywordType = keywordType;
this.keywords = keywords;
}
@Override
public void run(DBRProgressMonitor monitor) {
for (String keyword : keywords) {
info = readDataSourceHelp(monitor, dataSource, keywordType, keyword);
if (info != null) {
break;
}
}
if (CommonUtils.isEmpty(info)) {
info = "<b>" + keywords[0] + "</b> (" + keywordType.name() + ")";
}
}
}
private class TablesFinderJob extends DataSourceJob {
private final DBSStructureAssistant structureAssistant;
private final String[] containerNames;
private final String objectName;
private final ObjectLookupCache cache;
private final boolean caseSensitive;
protected TablesFinderJob(@NotNull DBCExecutionContext executionContext, @NotNull DBSStructureAssistant structureAssistant, @Nullable String[] containerNames, @NotNull String objectName, boolean caseSensitive, @NotNull ObjectLookupCache cache)
{
super("Find object '" + objectName + "'", DBeaverIcons.getImageDescriptor(UIIcon.SQL_EXECUTE), executionContext);
this.structureAssistant = structureAssistant;
// Transform container name case
this.containerNames = containerNames;
this.objectName = objectName;
this.caseSensitive = caseSensitive;
this.cache = cache;
setUser(false);
setSystem(true);
}
@Override
protected IStatus run(DBRProgressMonitor monitor)
{
cache.references = new ArrayList<>();
try {
DBSObjectContainer container = null;
if (!ArrayUtils.isEmpty(containerNames)) {
DBSObjectContainer dsContainer = DBUtils.getAdapter(DBSObjectContainer.class, getExecutionContext().getDataSource());
if (dsContainer != null) {
DBSObject childContainer = dsContainer.getChild(monitor, containerNames[0]);
if (childContainer instanceof DBSObjectContainer) {
container = (DBSObjectContainer) childContainer;
} else {
// Check in selected object
DBSObjectSelector dsSelector = DBUtils.getAdapter(DBSObjectSelector.class, getExecutionContext().getDataSource());
if (dsSelector != null) {
DBSObject curCatalog = dsSelector.getDefaultObject();
if (curCatalog instanceof DBSObjectContainer) {
childContainer = ((DBSObjectContainer)curCatalog).getChild(monitor, containerNames[0]);
}
}
if (childContainer == null) {
// Container is not direct child of schema/catalog. Let's try struct assistant
final List<DBSObjectReference> objReferences = structureAssistant.findObjectsByMask(monitor, null, structureAssistant.getAutoCompleteObjectTypes(), containerNames[0], false, true, 1);
if (objReferences.size() == 1) {
childContainer = objReferences.get(0).resolveObject(monitor);
}
if (childContainer == null) {
return Status.CANCEL_STATUS;
}
}
if (childContainer instanceof DBSObjectContainer) {
container = (DBSObjectContainer) childContainer;
}
}
}
}
if (container != null) {
if (containerNames.length > 1) {
// We have multiple containers. They MUST combine a unique
// path to the object
for (int i = 1; i < containerNames.length; i++) {
DBSObject childContainer = container.getChild(monitor, containerNames[i]);
if (childContainer instanceof DBSObjectContainer) {
container = (DBSObjectContainer) childContainer;
} else {
break;
}
}
} else {
// We have a container. But maybe it is a wrong one -
// this may happen if database supports multiple nested containers (catalog+schema+?)
// and schema name is the same as catalog name.
// So let's try to get nested container because we always need the deepest one.
DBSObject childContainer = container.getChild(monitor, containerNames[0]);
if (childContainer instanceof DBSObjectContainer) {
// Yep - this is it
container = (DBSObjectContainer) childContainer;
}
}
}
DBSObject targetObject = null;
if (container != null) {
final String fixedName = DBObjectNameCaseTransformer.transformName(getExecutionContext().getDataSource(), objectName);
if (fixedName != null) {
targetObject = container.getChild(monitor, fixedName);
}
}
if (targetObject != null) {
cache.references.add(new DirectObjectReference(container, null, targetObject));
} else {
DBSObjectType[] objectTypes = structureAssistant.getHyperlinkObjectTypes();
Collection<DBSObjectReference> objects = structureAssistant.findObjectsByMask(monitor, container, objectTypes, objectName, caseSensitive, false, 10);
if (!CommonUtils.isEmpty(objects)) {
cache.references.addAll(objects);
}
}
} catch (DBException e) {
log.warn(e);
}
finally {
cache.loading = false;
}
return Status.OK_STATUS;
}
}
}