blob: c9dfe2f9c9b950c69530feab5eb2046a7995a3c8 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2011 IBM Corporation and others.
* 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:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.internal.ui.text.java;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.osgi.framework.Bundle;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IContributor;
import org.eclipse.core.runtime.IExtension;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.InvalidRegistryObjectException;
import org.eclipse.core.runtime.PerformanceStats;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContextInformation;
import org.eclipse.jdt.internal.corext.util.Messages;
import org.eclipse.jdt.ui.text.IJavaPartitions;
import org.eclipse.jdt.ui.text.java.ContentAssistInvocationContext;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposalComputer;
import org.eclipse.jdt.internal.ui.JavaPlugin;
/**
* The description of an extension to the
* <code>org.eclipse.jdt.ui.javaCompletionProposalComputer</code> extension point. Instances are
* immutable. Instances can be obtained from a {@link CompletionProposalComputerRegistry}.
*
* @see CompletionProposalComputerRegistry
* @since 3.2
*/
final class CompletionProposalComputerDescriptor {
/** The default category id. */
private static final String DEFAULT_CATEGORY_ID= "org.eclipse.jdt.ui.defaultProposalCategory"; //$NON-NLS-1$
/** The extension schema name of the category id attribute. */
private static final String CATEGORY_ID= "categoryId"; //$NON-NLS-1$
/** The extension schema name of the partition type attribute. */
private static final String TYPE= "type"; //$NON-NLS-1$
/** The extension schema name of the class attribute. */
private static final String CLASS= "class"; //$NON-NLS-1$
/** The extension schema name of the activate attribute. */
private static final String ACTIVATE= "activate"; //$NON-NLS-1$
/** The extension schema name of the partition child elements. */
private static final String PARTITION= "partition"; //$NON-NLS-1$
/** Set of Java partition types. */
private static final Set<String> PARTITION_SET;
/** The name of the performance event used to trace extensions. */
private static final String PERFORMANCE_EVENT= JavaPlugin.getPluginId() + "/perf/content_assist/extensions"; //$NON-NLS-1$
/**
* If <code>true</code>, execution time of extensions is measured and the data forwarded to
* core's {@link PerformanceStats} service.
*/
private static final boolean MEASURE_PERFORMANCE= PerformanceStats.isEnabled(PERFORMANCE_EVENT);
/**
* Independently of the {@link PerformanceStats} service, any operation that takes longer than
* {@value} milliseconds will be flagged as an violation. This timeout does not apply to the
* first invocation, as it may take longer due to plug-in initialization etc. See also
* {@link #fIsReportingDelay}.
*/
private static final long MAX_DELAY= 5000;
/* log constants */
private static final String COMPUTE_COMPLETION_PROPOSALS= "computeCompletionProposals()"; //$NON-NLS-1$
private static final String COMPUTE_CONTEXT_INFORMATION= "computeContextInformation()"; //$NON-NLS-1$
private static final String SESSION_STARTED= "sessionStarted()"; //$NON-NLS-1$
private static final String SESSION_ENDED= "sessionEnded()"; //$NON-NLS-1$
static {
Set<String> partitions= new HashSet<String>();
partitions.add(IDocument.DEFAULT_CONTENT_TYPE);
partitions.add(IJavaPartitions.JAVA_DOC);
partitions.add(IJavaPartitions.JAVA_MULTI_LINE_COMMENT);
partitions.add(IJavaPartitions.JAVA_SINGLE_LINE_COMMENT);
partitions.add(IJavaPartitions.JAVA_STRING);
partitions.add(IJavaPartitions.JAVA_CHARACTER);
PARTITION_SET= Collections.unmodifiableSet(partitions);
}
/** The identifier of the extension. */
private final String fId;
/** The name of the extension. */
private final String fName;
/** The class name of the provided <code>IJavaCompletionProposalComputer</code>. */
private final String fClass;
/** The activate attribute value. */
private final boolean fActivate;
/** The partition of the extension (element type: {@link String}). */
private final Set<String> fPartitions;
/** The configuration element of this extension. */
private final IConfigurationElement fElement;
/** The registry we are registered with. */
private final CompletionProposalComputerRegistry fRegistry;
/** The computer, if instantiated, <code>null</code> otherwise. */
private IJavaCompletionProposalComputer fComputer;
/** The ui category. */
private final CompletionProposalCategory fCategory;
/** The first error message in the most recent operation, or <code>null</code>. */
private String fLastError;
/**
* Tells whether to inform the user when <code>MAX_DELAY</code> has been exceeded.
* We start timing execution after the first session because the first may take
* longer due to plug-in activation and initialization.
*/
private boolean fIsReportingDelay= false;
/** The start of the last operation. */
private long fStart;
/**
* Tells whether we tried to load the computer.
* @since 3.4
*/
boolean fTriedLoadingComputer= false;
/**
* Creates a new descriptor.
*
* @param element the configuration element to read
* @param registry the computer registry creating this descriptor
* @param categories the categories
* @throws InvalidRegistryObjectException if this extension is no longer valid
* @throws CoreException if the extension contains invalid values
*/
CompletionProposalComputerDescriptor(IConfigurationElement element, CompletionProposalComputerRegistry registry, List<CompletionProposalCategory> categories) throws InvalidRegistryObjectException, CoreException {
Assert.isLegal(registry != null);
Assert.isLegal(element != null);
fRegistry= registry;
fElement= element;
IExtension extension= element.getDeclaringExtension();
fId= extension.getUniqueIdentifier();
checkNotNull(fId, "id"); //$NON-NLS-1$
String name= extension.getLabel();
if (name.length() == 0)
fName= fId;
else
fName= name;
Set<String> partitions= new HashSet<String>();
IConfigurationElement[] children= element.getChildren(PARTITION);
if (children.length == 0) {
fPartitions= PARTITION_SET; // add to all partition types if no partition is configured
} else {
for (int i= 0; i < children.length; i++) {
String type= children[i].getAttribute(TYPE);
checkNotNull(type, TYPE);
partitions.add(type);
}
fPartitions= Collections.unmodifiableSet(partitions);
}
String activateAttribute= element.getAttribute(ACTIVATE);
fActivate= Boolean.valueOf(activateAttribute).booleanValue();
fClass= element.getAttribute(CLASS);
checkNotNull(fClass, CLASS);
String categoryId= element.getAttribute(CATEGORY_ID);
if (categoryId == null)
categoryId= DEFAULT_CATEGORY_ID;
CompletionProposalCategory category= null;
for (Iterator<CompletionProposalCategory> it= categories.iterator(); it.hasNext();) {
CompletionProposalCategory cat= it.next();
if (cat.getId().equals(categoryId)) {
category= cat;
break;
}
}
if (category == null) {
// create a category if it does not exist
fCategory= new CompletionProposalCategory(categoryId, fName, registry);
categories.add(fCategory);
} else {
fCategory= category;
}
}
/**
* Checks that the given attribute value is not <code>null</code>.
*
* @param value the object to check if not null
* @param attribute the attribute
* @throws InvalidRegistryObjectException if the registry element is no longer valid
* @throws CoreException if <code>value</code> is <code>null</code>
*/
private void checkNotNull(Object value, String attribute) throws InvalidRegistryObjectException, CoreException {
if (value == null) {
Object[] args= { getId(), fElement.getContributor().getName(), attribute };
String message= Messages.format(JavaTextMessages.CompletionProposalComputerDescriptor_illegal_attribute_message, args);
IStatus status= new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, message, null);
throw new CoreException(status);
}
}
/**
* Returns the identifier of the described extension.
*
* @return Returns the id
*/
public String getId() {
return fId;
}
/**
* Returns the name of the described extension.
*
* @return Returns the name
*/
public String getName() {
return fName;
}
/**
* Returns the partition types of the described extension.
*
* @return the set of partition types (element type: {@link String})
*/
public Set<String> getPartitions() {
return fPartitions;
}
/**
* Returns a cached instance of the computer as described in the
* extension's xml. If the computer is not yet created and
* <code>canCreate</code> is <code>true</code> then {@link #createComputer()}
* is called and the result cached.
*
* @param canCreate <code>true</code> if the proposal computer can be created
* @return a new instance of the completion proposal computer as
* described by this descriptor
* @throws CoreException if the creation fails
* @throws InvalidRegistryObjectException if the extension is not
* valid any longer (e.g. due to plug-in unloading)
*/
private synchronized IJavaCompletionProposalComputer getComputer(boolean canCreate) throws CoreException, InvalidRegistryObjectException {
if (fComputer == null && canCreate && !fTriedLoadingComputer && (fActivate || isPluginLoaded())) {
fTriedLoadingComputer= true;
fComputer= createComputer();
}
return fComputer;
}
private boolean isPluginLoaded() {
Bundle bundle= getBundle();
return bundle != null && bundle.getState() == Bundle.ACTIVE;
}
private Bundle getBundle() {
String namespace= fElement.getDeclaringExtension().getContributor().getName();
Bundle bundle= Platform.getBundle(namespace);
return bundle;
}
/**
* Returns a new instance of the computer as described in the
* extension's xml. Note that the safest way to access the computer
* is by using the
* {@linkplain #computeCompletionProposals(ContentAssistInvocationContext, IProgressMonitor) computeCompletionProposals}
* and
* {@linkplain #computeContextInformation(ContentAssistInvocationContext, IProgressMonitor) computeContextInformation}
* methods. These delegate the functionality to the contributed
* computer, but handle instance creation and any exceptions thrown.
*
* @return a new instance of the completion proposal computer as
* described by this descriptor
* @throws CoreException if the creation fails
* @throws InvalidRegistryObjectException if the extension is not
* valid any longer (e.g. due to plug-in unloading)
*/
public IJavaCompletionProposalComputer createComputer() throws CoreException, InvalidRegistryObjectException {
return (IJavaCompletionProposalComputer) fElement.createExecutableExtension(CLASS);
}
/**
* Safely computes completion proposals through the described extension. If the extension
* is disabled, throws an exception or otherwise does not adhere to the contract described in
* {@link IJavaCompletionProposalComputer}, an empty list is returned.
*
* @param context the invocation context passed on to the extension
* @param monitor the progress monitor passed on to the extension
* @return the list of computed completion proposals (element type:
* {@link org.eclipse.jface.text.contentassist.ICompletionProposal})
*/
public List<ICompletionProposal> computeCompletionProposals(ContentAssistInvocationContext context, IProgressMonitor monitor) {
if (!isEnabled())
return Collections.emptyList();
IStatus status;
try {
IJavaCompletionProposalComputer computer= getComputer(true);
if (computer == null) // not active yet
return Collections.emptyList();
try {
PerformanceStats stats= startMeter(context, computer);
List<ICompletionProposal> proposals= computer.computeCompletionProposals(context, monitor);
stopMeter(stats, COMPUTE_COMPLETION_PROPOSALS);
if (proposals != null) {
fLastError= computer.getErrorMessage();
return proposals;
}
} finally {
fIsReportingDelay= true;
}
status= createAPIViolationStatus(COMPUTE_COMPLETION_PROPOSALS);
} catch (InvalidRegistryObjectException x) {
status= createExceptionStatus(x);
} catch (CoreException x) {
status= createExceptionStatus(x);
} catch (RuntimeException x) {
status= createExceptionStatus(x);
} finally {
monitor.done();
}
fRegistry.informUser(this, status);
return Collections.emptyList();
}
/**
* Safely computes context information objects through the described extension. If the extension
* is disabled, throws an exception or otherwise does not adhere to the contract described in
* {@link IJavaCompletionProposalComputer}, an empty list is returned.
*
* @param context the invocation context passed on to the extension
* @param monitor the progress monitor passed on to the extension
* @return the list of computed context information objects (element type:
* {@link org.eclipse.jface.text.contentassist.IContextInformation})
*/
public List<IContextInformation> computeContextInformation(ContentAssistInvocationContext context, IProgressMonitor monitor) {
if (!isEnabled())
return Collections.emptyList();
IStatus status;
try {
IJavaCompletionProposalComputer computer= getComputer(true);
if (computer == null) // not active yet
return Collections.emptyList();
PerformanceStats stats= startMeter(context, computer);
List<IContextInformation> proposals= computer.computeContextInformation(context, monitor);
stopMeter(stats, COMPUTE_CONTEXT_INFORMATION);
if (proposals != null) {
fLastError= computer.getErrorMessage();
return proposals;
}
status= createAPIViolationStatus(COMPUTE_CONTEXT_INFORMATION);
} catch (InvalidRegistryObjectException x) {
status= createExceptionStatus(x);
} catch (CoreException x) {
status= createExceptionStatus(x);
} catch (RuntimeException x) {
status= createExceptionStatus(x);
} finally {
monitor.done();
}
fRegistry.informUser(this, status);
return Collections.emptyList();
}
/**
* Notifies the described extension of a proposal computation session start.
* <p><em>
* Note: This method is called every time code assist is invoked and
* is <strong>not</strong> filtered by partition type.
* </em></p>
*/
public void sessionStarted() {
if (!isEnabled())
return;
IStatus status;
try {
IJavaCompletionProposalComputer computer= getComputer(true);
if (computer == null) // not active yet
return;
PerformanceStats stats= startMeter(SESSION_STARTED, computer);
computer.sessionStarted();
stopMeter(stats, SESSION_ENDED);
return;
} catch (InvalidRegistryObjectException x) {
status= createExceptionStatus(x);
} catch (CoreException x) {
status= createExceptionStatus(x);
} catch (RuntimeException x) {
status= createExceptionStatus(x);
}
fRegistry.informUser(this, status);
}
/**
* Notifies the described extension of a proposal computation session end.
* <p><em>
* Note: This method is called every time code assist is invoked and
* is <strong>not</strong> filtered by partition type.
* </em></p>
*/
public void sessionEnded() {
if (!isEnabled())
return;
IStatus status;
try {
IJavaCompletionProposalComputer computer= getComputer(false);
if (computer == null) // not active yet
return;
PerformanceStats stats= startMeter(SESSION_ENDED, computer);
computer.sessionEnded();
stopMeter(stats, SESSION_ENDED);
return;
} catch (InvalidRegistryObjectException x) {
status= createExceptionStatus(x);
} catch (CoreException x) {
status= createExceptionStatus(x);
} catch (RuntimeException x) {
status= createExceptionStatus(x);
}
fRegistry.informUser(this, status);
}
private PerformanceStats startMeter(Object context, IJavaCompletionProposalComputer computer) {
final PerformanceStats stats;
if (MEASURE_PERFORMANCE) {
stats= PerformanceStats.getStats(PERFORMANCE_EVENT, computer);
stats.startRun(context.toString());
} else {
stats= null;
}
if (fIsReportingDelay) {
fStart= System.currentTimeMillis();
}
return stats;
}
private void stopMeter(final PerformanceStats stats, String operation) {
if (MEASURE_PERFORMANCE) {
stats.endRun();
if (stats.isFailure()) {
IStatus status= createPerformanceStatus(operation);
fRegistry.informUser(this, status);
return;
}
}
if (fIsReportingDelay) {
long current= System.currentTimeMillis();
if (current - fStart > MAX_DELAY) {
IStatus status= createPerformanceStatus(operation);
fRegistry.informUser(this, status);
}
}
}
private IStatus createExceptionStatus(InvalidRegistryObjectException x) {
// extension has become invalid - log & disable
String blame= createBlameMessage();
String reason= JavaTextMessages.CompletionProposalComputerDescriptor_reason_invalid;
return new Status(IStatus.INFO, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
}
private IStatus createExceptionStatus(CoreException x) {
// unable to instantiate the extension - log
String blame= createBlameMessage();
String reason= JavaTextMessages.CompletionProposalComputerDescriptor_reason_instantiation;
return new Status(IStatus.ERROR, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
}
private IStatus createExceptionStatus(RuntimeException x) {
// misbehaving extension - log
String blame= createBlameMessage();
String reason= JavaTextMessages.CompletionProposalComputerDescriptor_reason_runtime_ex;
return new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, x); //$NON-NLS-1$
}
private IStatus createAPIViolationStatus(String operation) {
String blame= createBlameMessage();
Object[] args= {operation};
String reason= Messages.format(JavaTextMessages.CompletionProposalComputerDescriptor_reason_API, args);
return new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, null); //$NON-NLS-1$
}
private IStatus createPerformanceStatus(String operation) {
String blame= createBlameMessage();
Object[] args= {operation};
String reason= Messages.format(JavaTextMessages.CompletionProposalComputerDescriptor_reason_performance, args);
return new Status(IStatus.WARNING, JavaPlugin.getPluginId(), IStatus.OK, blame + " " + reason, null); //$NON-NLS-1$
}
private String createBlameMessage() {
Object[] args= { getName(), fElement.getDeclaringExtension().getContributor().getName() };
String disable= Messages.format( JavaTextMessages.CompletionProposalComputerDescriptor_blame_message, args);
return disable;
}
/**
* Returns the enablement state of the described extension.
*
* @return the enablement state of the described extension
*/
private boolean isEnabled() {
return fCategory.isEnabled();
}
CompletionProposalCategory getCategory() {
return fCategory;
}
/**
* Returns the error message from the described extension.
*
* @return the error message from the described extension
*/
public String getErrorMessage() {
return fLastError;
}
/**
* Returns the contributor of the described extension.
*
* @return the contributor of the described extension
*/
IContributor getContributor() {
try {
return fElement.getContributor();
} catch (InvalidRegistryObjectException e) {
return null;
}
}
}