/*=============================================================================# # Copyright (c) 2014-2016 Stephan Wahlbrink (WalWare.de) 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: # Stephan Wahlbrink - initial API and implementation #=============================================================================*/ package de.walware.docmlet.wikitext.internal.core; import java.util.ArrayList; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.ProjectScope; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.content.IContentDescription; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.preferences.IEclipsePreferences; import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener; import org.eclipse.core.runtime.preferences.IEclipsePreferences.PreferenceChangeEvent; import org.eclipse.osgi.util.NLS; import de.walware.jcommons.collections.CopyOnWriteIdentityListSet; import de.walware.jcommons.collections.ImCollections; import de.walware.jcommons.collections.ImList; import de.walware.docmlet.wikitext.core.WikitextCore; import de.walware.docmlet.wikitext.core.markup.IMarkupConfig; import de.walware.docmlet.wikitext.core.markup.IMarkupLanguage; import de.walware.docmlet.wikitext.core.markup.IMarkupLanguageManager1; public class MarkupLanguageManager1 extends MarkupLanguageManager implements IMarkupLanguageManager1, IResourceChangeListener { public static final IMarkupLanguageManager1 INSTANCE; static { final MarkupLanguageManager1 instance= new MarkupLanguageManager1(); instance.addConfigChangedListener(new MarkupConfigTextFileBufferUpdater(instance)); INSTANCE= instance; } private static final String NODE_QUALIFIER= WikitextCore.PLUGIN_ID + "/markup/Wikitext"; //$NON-NLS-1$ private static final String KEY= "MarkupConfig"; //$NON-NLS-1$ private static final QualifiedName PROPERTY_NAME= new QualifiedName(WikitextCore.PLUGIN_ID, "Wikitext." + KEY); private static final String PREFIX= KEY + '!'; private static class Property { final String languageName; final IMarkupLanguage language; public Property(final String languageName, final IMarkupLanguage language) { this.languageName= languageName; this.language= language; } } private static String getPrefKey(final IFile file) { return PREFIX + file.getProjectRelativePath().toPortableString(); } private class ProjectEntry implements IPreferenceChangeListener { private final IProject project; private final IEclipsePreferences prefNode; private IFile tmpFile; private Property tmpFileProperty; public ProjectEntry(final IProject project) { this.project= project; final ProjectScope projectScope= new ProjectScope(project); this.prefNode= projectScope.getNode(NODE_QUALIFIER); this.prefNode.addPreferenceChangeListener(this); } public IProject getProject() { return this.project; } public IEclipsePreferences getPrefNode() { return this.prefNode; } public void dispose() { this.prefNode.removePreferenceChangeListener(this); } public void setTmpFileProperty(final IFile file, final Property property) { this.tmpFile= file; this.tmpFileProperty= property; } @Override public void preferenceChange(final PreferenceChangeEvent event) { if (event.getKey().startsWith(PREFIX) && this.project.isOpen()) { final IPath path= Path.fromPortableString(event.getKey().substring(PREFIX.length())); IFile file= null; synchronized (this) { Property property= null; if (this.tmpFile != null && this.tmpFile.getFullPath().equals(path)) { file= this.tmpFile; property= this.tmpFileProperty; this.tmpFile= null; this.tmpFileProperty= null; } else { file= this.project.getFile(path); } try { file.setSessionProperty(PROPERTY_NAME, property); } catch (final CoreException e) {} } synchronized (MarkupLanguageManager1.this.backgroundJob) { MarkupLanguageManager1.this.backgroundJob.addChangedConfig(file); MarkupLanguageManager1.this.backgroundJob.schedule(); } } } } private class BackgroundJob extends Job { private List<IProject> projectsToDispose; private List<IFile> changedFiles; private Map<String, List<IProject>> changedLanguages; public BackgroundJob() { super("Markup Config Worker"); setUser(false); setSystem(true); setPriority(SHORT); } public void addProjectToDispose(final IProject project) { if (this.projectsToDispose == null) { this.projectsToDispose= new ArrayList<>(); } this.projectsToDispose.add(project); } public void addChangedConfig(final String languageName) { if (this.changedLanguages == null) { this.changedLanguages= new IdentityHashMap<>(); } this.changedLanguages.put(languageName, ImCollections.<IProject>newList()); } public void addChangedConfig(final IProject project, final String languageName) { if (this.changedLanguages == null) { this.changedLanguages= new IdentityHashMap<>(); } List<IProject> projects= this.changedLanguages.get(languageName); if (projects == null) { projects= new ArrayList<>(); this.changedLanguages.put(languageName, projects); } else if (projects.isEmpty()) { return; } if (!projects.contains(project)) { projects.add(project); } } public void addChangedConfig(final IFile file) { if (this.changedFiles == null) { this.changedFiles= new ArrayList<>(); } this.changedFiles.add(file); } @Override protected IStatus run(final IProgressMonitor monitor) { final SubMonitor m= SubMonitor.convert(monitor); execDispose(); execNotify(m); return Status.OK_STATUS; } private void execDispose() { while (true) { final List<IProject> projects; synchronized (this) { projects= this.projectsToDispose; this.projectsToDispose= null; } if (projects != null) { synchronized (projects) { for (final IProject project : projects) { final ProjectEntry entry= MarkupLanguageManager1.this.projectEntries.remove(project); if (entry != null) { entry.dispose(); } } } } } } private void execNotify(final SubMonitor m) { final List<IFile> files; final Map<String, List<IProject>> changedLanguages; synchronized (this) { files= this.changedFiles; this.changedFiles= null; changedLanguages= this.changedLanguages; this.changedLanguages= null; } m.setWorkRemaining(((changedLanguages != null) ? changedLanguages.size() * 10 : 0) + ((files != null) ? files.size() : 0) ); if (changedLanguages != null) { final SubMonitor mLanguages= m.newChild(changedLanguages.size() * 10); final ImList<IMarkupConfigChangedListener> listeners= MarkupLanguageManager1.this.configChangedListeners.toList(); mLanguages.setWorkRemaining(listeners.size()); for (final IMarkupConfigChangedListener listener : listeners) { try { listener.configChanged(changedLanguages, mLanguages.newChild(1)); } catch (final CoreException e) { } } } if (files != null) { for (final IFile file : files) { final SubMonitor mFile= m.newChild(1); if (file.exists()) { final ImList<IMarkupConfigChangedListener> listeners= MarkupLanguageManager1.this.configChangedListeners.toList(); mFile.setWorkRemaining(listeners.size()); for (final IMarkupConfigChangedListener listener : listeners) { try { listener.configChanged(file, mFile.newChild(1)); } catch (final CoreException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } } } } } private final CopyOnWriteIdentityListSet<IMarkupConfigChangedListener> configChangedListeners= new CopyOnWriteIdentityListSet<>(); private final Map<IProject, ProjectEntry> projectEntries= new HashMap<>(); private final BackgroundJob backgroundJob= new BackgroundJob(); public MarkupLanguageManager1() { ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.PRE_DELETE ); } private String getLanguageName(final IFile file, final boolean required) throws CoreException { Exception cause= null; try { final IContentDescription contentDescription= file.getContentDescription(); if (contentDescription != null) { return getLanguageName(contentDescription.getContentType()); } } catch (final CoreException e) { cause= e; } if (required) { throw new CoreException(new Status(IStatus.ERROR, WikitextCore.PLUGIN_ID, NLS.bind("Failed to detect markup language for file ''{0}''.", file.getFullPath()), cause )); } else { return null; } } private ProjectEntry getProjectEntry(final IProject project) { synchronized (this.projectEntries) { ProjectEntry entry= this.projectEntries.get(project); if (entry == null) { entry= new ProjectEntry(project); this.projectEntries.put(project, entry); } return entry; } } @Override public void resourceChanged(final IResourceChangeEvent event) { final IResource resource= event.getResource(); if (resource instanceof IResource) { synchronized (this.backgroundJob) { this.backgroundJob.addProjectToDispose((IProject) resource); this.backgroundJob.schedule(100); } } } @Override public void addConfigChangedListener(final IMarkupConfigChangedListener listener) { this.configChangedListeners.add(listener); } @Override public void removeConfigChangedListern(final IMarkupConfigChangedListener listener) { this.configChangedListeners.remove(listener); } @Override public IMarkupLanguage getLanguage(final IFile file, String languageName, final boolean inherit) { if (file == null) { throw new NullPointerException("file"); //$NON-NLS-1$ } try { final boolean exists= file.exists(); Property property= null; String configString= null; if (exists) { property= (Property) file.getSessionProperty(PROPERTY_NAME); } if (property == null || (languageName != null && property.languageName != languageName)) { final ProjectEntry projectEntry= getProjectEntry(file.getProject()); final IEclipsePreferences prefNode= projectEntry.getPrefNode(); configString= prefNode.get(getPrefKey(file), null); if (property == null) { final String fileLanguageName= getLanguageName(file, (languageName == null)); if (exists && fileLanguageName != null) { property= new Property(fileLanguageName, (configString != null) ? getLanguage(fileLanguageName, configString) : null ); synchronized (projectEntry) { if (prefNode.get(getPrefKey(file), null) == configString) { file.setSessionProperty(PROPERTY_NAME, property); } } } if (languageName == null) { languageName= fileLanguageName; } } if (property == null || property.languageName != languageName) { return (configString != null || inherit) ? getLanguage(languageName, configString) : null; } } return (property.language != null || !inherit) ? property.language : getLanguage(file.getProject(), property.languageName, true); } catch (final Exception e) { WikitextCorePlugin.log(new Status(IStatus.ERROR, WikitextCore.PLUGIN_ID, NLS.bind("An error occurred when occurred when reading markup configuration for ''{0}''.", file.getFullPath() ), e )); return null; } } private IMarkupLanguage getLanguage(final String name, final String configString) { if (configString != null) { final IMarkupConfig config= createNewConfig(name); if (config != null && config.load(configString)) { return createLanguage(name, config); } } return getLanguage(name); } private IMarkupLanguage createLanguage(final String name, final IMarkupConfig config) { final IMarkupLanguage language= getLanguage(name).clone(); language.setMarkupConfig(config); return language; } @Override public IMarkupConfig getConfig(final String languageName) { final IMarkupLanguage language= getLanguage(languageName); if (language == null) { throw new IllegalStateException("Language is missing: " + languageName); } return language.getMarkupConfig(); } @Override protected void configChanged(final String languageName) { super.configChanged(languageName); synchronized (this.backgroundJob) { this.backgroundJob.addChangedConfig(languageName); this.backgroundJob.schedule(100); } } public IMarkupLanguage getLanguage(final IProject project, final String languageName, final boolean inherit) { return (inherit) ? getLanguage(languageName) : null; } @Override public void setConfig(final IFile file, final IMarkupConfig config) { if (file == null) { throw new NullPointerException("file"); //$NON-NLS-1$ } try { final ProjectEntry projectEntry= getProjectEntry(file.getProject()); final IEclipsePreferences prefNode= projectEntry.getPrefNode(); if (config == null) { synchronized (projectEntry) { projectEntry.setTmpFileProperty(file, null); prefNode.remove(getPrefKey(file)); } } else { final String languageName= getLanguageName(file, true); final Property property= new Property(languageName, createLanguage(languageName, config) ); final String value= config.getString(); synchronized (projectEntry) { projectEntry.setTmpFileProperty(file, property); prefNode.put(getPrefKey(file), value); } } prefNode.flush(); } catch (final Exception e) { WikitextCorePlugin.log(new Status(IStatus.ERROR, WikitextCore.PLUGIN_ID, NLS.bind("An error occurred when occurred when saving markup configuration for ''{0}''.", file.getFullPath().toFile() ), e )); } } @Override public IMarkupConfig getConfig(final IFile file, final String languageName) { final IMarkupLanguage language= getLanguage(file, languageName, false); return (language != null) ? language.getMarkupConfig() : null; } }