| /*=============================================================================# |
| # 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; |
| } |
| } |
| |
| } |