blob: e3902a61efde87cc917e377b183db7cac221fc46 [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2009, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.ltk.core;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import org.eclipse.core.filesystem.EFS;
import org.eclipse.core.filesystem.IFileStore;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Platform;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.content.IContentDescription;
import org.eclipse.core.runtime.content.IContentType;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.osgi.util.NLS;
import org.eclipse.statet.jcommons.collections.ImCollections;
import org.eclipse.statet.jcommons.collections.ImList;
import org.eclipse.statet.jcommons.lang.Disposable;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.ltk.core.IExtContentTypeManager;
import org.eclipse.statet.ltk.core.Ltk;
import org.eclipse.statet.ltk.core.WorkingContext;
import org.eclipse.statet.ltk.model.core.ModelTypeDescriptor;
import org.eclipse.statet.ltk.model.core.SourceUnitFactory;
import org.eclipse.statet.ltk.model.core.SourceUnitManager;
import org.eclipse.statet.ltk.model.core.element.LtkModelElement;
import org.eclipse.statet.ltk.model.core.element.SourceUnit;
@NonNullByDefault
public class SourceUnitManagerImpl implements SourceUnitManager, Disposable {
private static final String CONFIG_MODELTYPE_ID_ATTRIBUTE_NAME= "modelTypeId"; //$NON-NLS-1$
private static final String CONFIG_CONTEXT_KEY_ATTRIBUTE_NAME= "contextKey"; //$NON-NLS-1$
private static final class SuItem extends SoftReference<SourceUnit> {
private final String key;
public SuItem(final String key, final SourceUnit su, final ReferenceQueue<SourceUnit> queue) {
super(su, queue);
this.key= key;
}
public String getKey() {
return this.key;
}
public void dispose() {
final SourceUnit su= get();
if (su != null && su.isConnected()) {
LtkCorePlugin.log(
new Status(IStatus.WARNING, LtkCorePlugin.BUNDLE_ID, -1,
NLS.bind("Source Unit ''{0}'' disposed but connected.", su.getId()), null));
}
clear();
}
}
private static class ContextItem {
private final WorkingContext context;
private final SourceUnitFactory factory;
private final HashMap<String, SuItem> sus;
private final ReferenceQueue<SourceUnit> susToClean;
public ContextItem(final WorkingContext context, final SourceUnitFactory factory) {
this.context= context;
this.factory= factory;
this.sus= new HashMap<>();
this.susToClean= new ReferenceQueue<>();
}
@Override
public int hashCode() {
return this.context.hashCode();
}
@Override
public boolean equals(final @Nullable Object obj) {
if (obj instanceof ContextItem) {
return ( ((ContextItem)obj).context == this.context);
}
return false;
}
public synchronized @Nullable SourceUnit getOpenSu(final Object from) {
final String id= this.factory.createId(from);
if (id != null) {
final SuItem suItem= this.sus.get(id);
if (suItem != null) {
final SourceUnit su= suItem.get();
if (su != null && !suItem.isEnqueued()) {
return su;
}
}
}
return null;
}
public synchronized void appendOpenSus(final ArrayList<SourceUnit> list) {
final Collection<SuItem> suItems= this.sus.values();
list.ensureCapacity(list.size() + suItems.size());
for (final SuItem suItem : suItems) {
final SourceUnit su= suItem.get();
if (su != null && !suItem.isEnqueued()) {
list.add(su);
}
}
}
}
private static class ModelItem {
private final String modelTypeId;
private volatile ImList<ContextItem> contextItems= ImCollections.newList();
public ModelItem(final String modelTypeId) {
this.modelTypeId= modelTypeId;
}
public @Nullable ContextItem getContextItem(final WorkingContext context, final boolean create) {
final ImList<ContextItem> contextItems= this.contextItems;
for (final ContextItem contextItem : contextItems) {
if (contextItem.context == context) {
return contextItem;
}
}
if (create) {
synchronized (this) {
if (contextItems != this.contextItems) {
return getContextItem(context, true);
}
try {
final IConfigurationElement[] elements= Platform.getExtensionRegistry().
getConfigurationElementsFor("org.eclipse.statet.ltk.ModelTypes"); //$NON-NLS-1$
IConfigurationElement matchingElement= null;
for (final IConfigurationElement element : elements) {
if (element.getName().equals("unitType") && element.isValid()) { //$NON-NLS-1$
final String typeIdOfElement= element.getAttribute(CONFIG_MODELTYPE_ID_ATTRIBUTE_NAME);
final String contextKeyOfElement= element.getAttribute(CONFIG_CONTEXT_KEY_ATTRIBUTE_NAME);
if (this.modelTypeId.equals(typeIdOfElement)) {
if ((contextKeyOfElement == null) || (contextKeyOfElement.length() == 0)) {
matchingElement= element;
continue;
}
if (contextKeyOfElement.equals(context.getKey())) {
matchingElement= element;
break;
}
}
}
}
if (matchingElement != null) {
final SourceUnitFactory factory= (SourceUnitFactory)matchingElement.createExecutableExtension("unitFactory"); //$NON-NLS-1$
final ContextItem contextItem= new ContextItem(context, factory);
this.contextItems= ImCollections.addElement(contextItems, contextItem);
return contextItem;
}
}
catch (final Exception e) {
LtkCorePlugin.log(new Status(IStatus.ERROR, Ltk.BUNDLE_ID, 0,
"Error loading working context contributions", e )); //$NON-NLS-1$
}
}
}
return null;
}
public ImList<ContextItem> getOpenContextItems(final @Nullable WorkingContext context) {
final ImList<ContextItem> contextItems= this.contextItems;
if (context != null) {
for (final ContextItem contextItem : contextItems) {
if (contextItem.context == context) {
return ImCollections.newList(contextItem);
}
}
return ImCollections.emptyList();
}
else {
return contextItems;
}
}
@Override
public int hashCode() {
return this.modelTypeId.hashCode();
}
@Override
public boolean equals(final @Nullable Object obj) {
return (obj instanceof ModelItem
&& this.modelTypeId.equals(((ModelItem)obj).modelTypeId));
}
}
private class CleanupJob extends Job {
private final Object scheduleLock= new Object();
public CleanupJob() {
super("SourceUnit Cleanup"); //$NON-NLS-1$
setUser(false);
setSystem(true);
setPriority(DECORATE);
}
void initialSchedule() {
synchronized (this.scheduleLock) {
schedule(180000);
}
}
void dispose() {
synchronized (this.scheduleLock) {
cancel();
}
}
@Override
protected IStatus run(final IProgressMonitor monitor) {
final int count= performCleanup();
synchronized (this.scheduleLock) {
if (monitor.isCanceled()) {
return Status.CANCEL_STATUS;
}
else {
schedule(count > 0 ? 60000 : 180000);
return Status.OK_STATUS;
}
}
}
}
private final CleanupJob cleanupJob= new CleanupJob();
private volatile ImList<ModelItem> modelItems= ImCollections.newList();
private final IExtContentTypeManager contentManager= Ltk.getExtContentTypeManager();
public SourceUnitManagerImpl() {
this.cleanupJob.initialSchedule();
}
private int performCleanup() {
int count= 0;
final ImList<ModelItem> modelItems= this.modelItems;
for (final ModelItem modelItem : modelItems) {
final List<ContextItem> contextItems= modelItem.contextItems;
for (final ContextItem contextItem : contextItems) {
SuItem suItem;
while ((suItem= (SuItem)contextItem.susToClean.poll()) != null){
synchronized (contextItem.sus) {
if (contextItem.sus.get(suItem.getKey()) == suItem) {
contextItem.sus.remove(suItem.getKey());
}
suItem.dispose();
count++;
}
}
}
}
return count;
}
@Override
public void dispose() {
this.cleanupJob.dispose();
}
@Override
public @Nullable SourceUnit getSourceUnit(final String modelTypeId, final WorkingContext context,
final Object from, final boolean create,
final IProgressMonitor monitor) {
if (modelTypeId == null) {
throw new NullPointerException("modelTypeId"); //$NON-NLS-1$
}
if (context == null) {
throw new NullPointerException("context"); //$NON-NLS-1$
}
return doGetSourceUnit(modelTypeId, context, from, create, monitor);
}
@Override
public @Nullable SourceUnit getSourceUnit(final WorkingContext context,
final Object from, @Nullable IContentType contentType, final boolean create,
final IProgressMonitor monitor) {
if (context == null) {
throw new NullPointerException("context"); //$NON-NLS-1$
}
String modelTypeId;
if (from instanceof SourceUnit) {
modelTypeId= ((SourceUnit)from).getModelTypeId();
}
else {
if (contentType == null) {
contentType= detectContentType(from);
if (contentType == null) {
return null;
}
}
final ModelTypeDescriptor modelType= this.contentManager.getModelTypeForContentType(contentType.getId());
if (modelType == null) {
return null;
}
modelTypeId= modelType.getId();
}
return doGetSourceUnit(modelTypeId, context, from, create, monitor);
}
private @Nullable SourceUnit doGetSourceUnit(final String modelTypeId, final WorkingContext context,
final Object from, final boolean create,
final IProgressMonitor monitor) {
final SourceUnit fromUnit= (from instanceof SourceUnit) ? ((SourceUnit)from) : null;
final ModelItem modelItem= getModelItem(modelTypeId);
final ContextItem contextItem= modelItem.getContextItem(context, create);
SourceUnit su= null;
if (contextItem != null) {
final String id= (fromUnit != null) ? fromUnit.getId() : contextItem.factory.createId(from);
if (id != null) {
synchronized (contextItem) {
SuItem suItem= contextItem.sus.get(id);
if (suItem != null) {
su= suItem.get();
if (suItem.isEnqueued()) {
su= null;
}
}
else {
if (create) {
su= contextItem.factory.createSourceUnit(id, from);
if (su == null || !su.getModelTypeId().equals(modelItem.modelTypeId)
|| (su.getElementType() & LtkModelElement.MASK_C1) != LtkModelElement.C1_SOURCE) {
// TODO log
return null;
}
suItem= new SuItem(id, su, contextItem.susToClean);
}
}
}
}
}
else {
if (create) {
throw new UnsupportedOperationException(NLS.bind(
"Missing factory for model type ''{0}''.", modelTypeId)); //$NON-NLS-1$
}
else {
return null;
}
}
if (su != null) {
su.connect(monitor);
if (fromUnit != null) {
fromUnit.disconnect(monitor);
}
return su;
}
else {
return null;
}
}
@Override
public List<SourceUnit> getOpenSourceUnits(final String modelTypeId,
final @Nullable WorkingContext context) {
final List<ModelItem> modelItems= getOpenModelItems(modelTypeId);
if (modelItems.isEmpty()) {
return Collections.emptyList();
}
final ArrayList<SourceUnit> list= new ArrayList<>();
for (int i= 0; i < modelItems.size(); i++) {
final ImList<ContextItem> contextItems= modelItems.get(i).getOpenContextItems(context);
if (contextItems.isEmpty()) {
continue;
}
for (final ContextItem contextItem : contextItems) {
contextItem.appendOpenSus(list);
}
}
return list;
}
@Override
public List<SourceUnit> getOpenSourceUnits(final List<String> modelTypeIds,
final @Nullable WorkingContext context) {
final List<ModelItem> includedModelItems= getOpenModelItems(modelTypeIds);
if (includedModelItems.isEmpty()) {
return Collections.emptyList();
}
final ArrayList<SourceUnit> list= new ArrayList<>(4);
for (int i= 0; i < includedModelItems.size(); i++) {
final ImList<ContextItem> contextItems= this.modelItems.get(i).getOpenContextItems(context);
if (contextItems.isEmpty()) {
continue;
}
for (final ContextItem contextItem : contextItems) {
contextItem.appendOpenSus(list);
}
}
return list;
}
@Override
public List<SourceUnit> getOpenSourceUnits(final List<String> modelTypeIds,
final @Nullable WorkingContext context, final Object from) {
final List<ModelItem> includedModelItems= getOpenModelItems(modelTypeIds);
if (includedModelItems.isEmpty()) {
return Collections.emptyList();
}
final ArrayList<SourceUnit> list= new ArrayList<>(4);
for (int i= 0; i < includedModelItems.size(); i++) {
final ImList<ContextItem> contextItems= this.modelItems.get(i).getOpenContextItems(context);
if (contextItems.isEmpty()) {
continue;
}
for (final ContextItem contextItem : contextItems) {
final SourceUnit su= contextItem.getOpenSu(from);
if (su != null) {
list.add(su);
}
}
}
return list;
}
private @Nullable IContentType detectContentType(final Object from) {
try {
if (from instanceof IFile) {
final IFile file= (IFile)from;
final IContentDescription contentDescription= file.getContentDescription();
if (contentDescription != null) {
return contentDescription.getContentType();
}
else {
return null;
}
}
else if (from instanceof IFileStore) {
final IFileStore file= (IFileStore)from;
try (final InputStream stream= file.openInputStream(EFS.NONE, null)) {
final IContentDescription contentDescription= Platform.getContentTypeManager()
.getDescriptionFor(stream, file.getName(), IContentDescription.ALL);
if (contentDescription != null) {
return contentDescription.getContentType();
}
else {
return null;
}
}
}
else {
return null;
}
}
catch (final CoreException | IOException | UnsupportedOperationException e) {
LtkCorePlugin.log(new Status(IStatus.ERROR, Ltk.BUNDLE_ID, 0,
"An error occurred when trying to detect content type of " + from,
e ));
return null;
}
}
private ModelItem getModelItem(final String modelTypeId) {
final ImList<ModelItem> modelItems= this.modelItems;
for (final ModelItem modelItem : modelItems) {
if (modelItem.modelTypeId == modelTypeId) {
return modelItem;
}
}
synchronized (this) {
if (modelItems != this.modelItems) {
return getModelItem(modelTypeId);
}
final ModelItem modelItem= new ModelItem(modelTypeId);
this.modelItems= ImCollections.addElement(modelItems, modelItem);
return modelItem;
}
}
private List<ModelItem> getOpenModelItems(final @Nullable String modelTypeId) {
final ImList<ModelItem> modelItems= this.modelItems;
if (modelTypeId != null) {
for (final ModelItem modelItem : modelItems) {
if (modelItem.modelTypeId == modelTypeId) {
return ImCollections.newList(modelItem);
}
}
return ImCollections.emptyList();
}
else {
return modelItems;
}
}
private List<ModelItem> getOpenModelItems(final @Nullable List<String> modelTypeIds) {
final ImList<ModelItem> modelItems= this.modelItems;
if (modelTypeIds != null) {
final List<ModelItem> matches= new ArrayList<>(modelTypeIds.size());
for (final ModelItem modelItem : modelItems) {
if (modelTypeIds.contains(modelItem.modelTypeId)) {
matches.add(modelItem);
}
}
return matches;
}
else {
return modelItems;
}
}
}