/*******************************************************************************
* Copyright (c) 2004, 2006 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.core.internal.resources;
import java.util.*;
import org.eclipse.core.internal.utils.Messages;
import org.eclipse.core.internal.utils.Policy;
import org.eclipse.core.resources.*;
import org.eclipse.core.runtime.*;
import org.eclipse.core.runtime.jobs.ISchedulingRule;
import org.eclipse.core.runtime.jobs.Job;
import org.osgi.framework.Bundle;
import org.osgi.service.prefs.BackingStoreException;
import org.osgi.service.prefs.Preferences;
/**
* Manages user-defined encodings as preferences in the project content area.
*
* @since 3.0
*/
public class CharsetManager implements IManager {
/**
* This job implementation is used to allow the resource change listener to schedule operations
* that need to modify the workspace.
*/
private class CharsetManagerJob extends Job {
private static final int CHARSET_UPDATE_DELAY= 500;
private List asyncChanges= new ArrayList();
public CharsetManagerJob() {
super(Messages.resources_charsetUpdating);
setSystem(true);
setPriority(Job.INTERACTIVE);
}
public void addChanges(Set newChanges) {
if (newChanges.isEmpty())
return;
synchronized (asyncChanges) {
asyncChanges.addAll(newChanges);
asyncChanges.notify();
}
schedule(CHARSET_UPDATE_DELAY);
}
public IProject getNextChange() {
synchronized (asyncChanges) {
return asyncChanges.isEmpty() ? null : (IProject)asyncChanges.remove(asyncChanges.size() - 1);
}
}
/* (non-Javadoc)
* @see org.eclipse.core.internal.jobs.InternalJob#run(org.eclipse.core.runtime.IProgressMonitor)
*/
protected IStatus run(IProgressMonitor monitor) {
MultiStatus result= new MultiStatus(ResourcesPlugin.PI_RESOURCES, IResourceStatus.FAILED_SETTING_CHARSET, Messages.resources_updatingEncoding, null);
monitor= Policy.monitorFor(monitor);
try {
monitor.beginTask(Messages.resources_charsetUpdating, Policy.totalWork);
final ISchedulingRule rule= workspace.getRuleFactory().modifyRule(workspace.getRoot());
try {
workspace.prepareOperation(rule, monitor);
workspace.beginOperation(true);
IProject next;
while ((next= getNextChange()) != null) {
//just exit if the system is shutting down or has been shut down
//it is too late to change the workspace at this point anyway
if (systemBundle.getState() != Bundle.ACTIVE)
return Status.OK_STATUS;
try {
if (next.isAccessible()) {
Preferences projectPrefs= getPreferences(next, false);
if (projectPrefs != null)
projectPrefs.flush();
}
} catch (BackingStoreException e) {
// we got an error saving
String detailMessage= Messages.resources_savingEncoding;
result.add(new ResourceStatus(IResourceStatus.FAILED_SETTING_CHARSET, next.getFullPath(), detailMessage, e));
}
}
monitor.worked(Policy.opWork);
} catch (OperationCanceledException e) {
workspace.getWorkManager().operationCanceled();
throw e;
} finally {
workspace.endOperation(rule, true, Policy.subMonitorFor(monitor, Policy.endOpWork));
}
} catch (CoreException ce) {
return ce.getStatus();
} finally {
monitor.done();
}
return result;
}
/* (non-Javadoc)
* @see org.eclipse.core.runtime.jobs.Job#shouldRun()
*/
public boolean shouldRun() {
synchronized (asyncChanges) {
return !asyncChanges.isEmpty();
}
}
}
class Listener implements IResourceChangeListener {
private void processEntryChanges(IResourceDelta projectDelta, Set projectsToSave) {
// check each resource with user-set encoding to see if it has
// been moved/deleted
boolean resourceChanges= false;
IProject currentProject= (IProject)projectDelta.getResource();
Preferences projectPrefs= getPreferences(currentProject, false);
if (projectPrefs == null)
// no preferences for this project, just bail
return;
String[] affectedResources;
try {
affectedResources= projectPrefs.keys();
} catch (BackingStoreException e) {
// problems with the project scope... we gonna miss the changes (but will log)
String message= Messages.resources_readingEncoding;
Policy.log(new ResourceStatus(IResourceStatus.FAILED_GETTING_CHARSET, currentProject.getFullPath(), message, e));
return;
}
for (int i= 0; i < affectedResources.length; i++) {
IResourceDelta memberDelta= projectDelta.findMember(new Path(affectedResources[i]));
// no changes for the given resource
if (memberDelta == null)
continue;
if (memberDelta.getKind() == IResourceDelta.REMOVED) {
resourceChanges= true;
// remove the setting for the original location - save its value though
String currentValue= projectPrefs.get(affectedResources[i], null);
projectPrefs.remove(affectedResources[i]);
if ((memberDelta.getFlags() & IResourceDelta.MOVED_TO) != 0) {
// if moving, copy the setting for the new location
IProject targetProject= workspace.getRoot().getProject(memberDelta.getMovedToPath().segment(0));
Preferences targetPrefs= getPreferences(targetProject, true);
targetPrefs.put(getKeyFor(memberDelta.getMovedToPath()), currentValue);
if (targetProject != currentProject)
projectsToSave.add(targetProject);
}
}
}
if (resourceChanges)
projectsToSave.add(currentProject);
}
/**
* For any change to the encoding file or any resource with encoding set, just discard the
* cache for the corresponding project.
*/
public void resourceChanged(IResourceChangeEvent event) {
IResourceDelta delta= event.getDelta();
if (delta == null)
return;
IResourceDelta[] projectDeltas= delta.getAffectedChildren();
// process each project in the delta
Set projectsToSave= new HashSet();
for (int i= 0; i < projectDeltas.length; i++)
//nothing to do if a project has been added/removed/moved
if (projectDeltas[i].getKind() == IResourceDelta.CHANGED && (projectDeltas[i].getFlags() & IResourceDelta.OPEN) == 0)
processEntryChanges(projectDeltas[i], projectsToSave);
job.addChanges(projectsToSave);
}
}
public static final String ENCODING_PREF_NODE= "encoding"; //$NON-NLS-1$
private static final String PROJECT_KEY= "<project>"; //$NON-NLS-1$
private CharsetDeltaJob charsetListener;
CharsetManagerJob job;
private IResourceChangeListener listener;
protected final Bundle systemBundle= Platform.getBundle("org.eclipse.osgi"); //$NON-NLS-1$
Workspace workspace;
public CharsetManager(Workspace workspace) {
this.workspace= workspace;
}
/**
* Returns the charset explicitly set by the user for the given resource, or <code>null</code>.
* If no setting exists for the given resource and <code>recurse</code> is <code>true</code>,
* every parent up to the workspace root will be checked until a charset setting can be found.
*
* @param resourcePath the path for the resource
* @param recurse whether the parent should be queried
* @return the charset setting for the given resource
*/
public String getCharsetFor(IPath resourcePath, boolean recurse) {
Assert.isLegal(resourcePath.segmentCount() >= 1);
IProject project= workspace.getRoot().getProject(resourcePath.segment(0));
Preferences encodingSettings= getPreferences(project, false);
if (encodingSettings == null)
// no preferences found - for performance reasons, short-circuit
// lookup by falling back to workspace's default setting
return recurse ? ResourcesPlugin.getEncoding() : null;
return internalGetCharsetFor(resourcePath, encodingSettings, recurse);
}
String getKeyFor(IPath resourcePath) {
return resourcePath.segmentCount() > 1 ? resourcePath.removeFirstSegments(1).toString() : PROJECT_KEY;
}
Preferences getPreferences(IProject project, boolean create) {
if (create)
// create all nodes down to the one we are interested in
return new ProjectScope(project).getNode(ResourcesPlugin.PI_RESOURCES).node(ENCODING_PREF_NODE);
// be careful looking up for our node so not to create any nodes as side effect
Preferences node= Platform.getPreferencesService().getRootNode().node(ProjectScope.SCOPE);
try {
//TODO once bug 90500 is fixed, should be as simple as this:
// String path = project.getName() + IPath.SEPARATOR + ResourcesPlugin.PI_RESOURCES + IPath.SEPARATOR + ENCODING_PREF_NODE;
// return node.nodeExists(path) ? node.node(path) : null;
// for now, take the long way
if (!node.nodeExists(project.getName()))
return null;
node= node.node(project.getName());
if (!node.nodeExists(ResourcesPlugin.PI_RESOURCES))
return null;
node= node.node(ResourcesPlugin.PI_RESOURCES);
if (!node.nodeExists(ENCODING_PREF_NODE))
return null;
return node.node(ENCODING_PREF_NODE);
} catch (BackingStoreException e) {
// nodeExists failed
String message= Messages.resources_readingEncoding;
Policy.log(new ResourceStatus(IResourceStatus.FAILED_GETTING_CHARSET, project.getFullPath(), message, e));
}
return null;
}
private String internalGetCharsetFor(IPath resourcePath, Preferences encodingSettings, boolean recurse) {
String charset= encodingSettings.get(getKeyFor(resourcePath), null);
if (!recurse)
return charset;
while (charset == null && resourcePath.segmentCount() > 1) {
resourcePath= resourcePath.removeLastSegments(1);
charset= encodingSettings.get(getKeyFor(resourcePath), null);
}
// ensure we default to the workspace encoding if none is found
return charset == null ? ResourcesPlugin.getEncoding() : charset;
}
public void projectPreferencesChanged(IProject project) {
charsetListener.charsetPreferencesChanged(project);
}
public void setCharsetFor(IPath resourcePath, String newCharset) throws CoreException {
// for the workspace root we just set a preference in the instance scope
if (resourcePath.segmentCount() == 0) {
org.eclipse.core.runtime.Preferences resourcesPreferences= ResourcesPlugin.getPlugin().getPluginPreferences();
if (newCharset != null)
resourcesPreferences.setValue(ResourcesPlugin.PREF_ENCODING, newCharset);
else
resourcesPreferences.setToDefault(ResourcesPlugin.PREF_ENCODING);
ResourcesPlugin.getPlugin().savePluginPreferences();
return;
}
// for all other cases, we set a property in the corresponding project
IProject project= workspace.getRoot().getProject(resourcePath.segment(0));
Preferences encodingSettings= getPreferences(project, true);
if (newCharset == null || newCharset.trim().length() == 0)
encodingSettings.remove(getKeyFor(resourcePath));
else
encodingSettings.put(getKeyFor(resourcePath), newCharset);
try {
// disable the listener so we don't react to changes made by ourselves
charsetListener.setDisabled(true);
// save changes
encodingSettings.flush();
} catch (BackingStoreException e) {
String message= Messages.resources_savingEncoding;
throw new ResourceException(IResourceStatus.FAILED_SETTING_CHARSET, project.getFullPath(), message, e);
} finally {
charsetListener.setDisabled(false);
}
}
public void shutdown(IProgressMonitor monitor) {
workspace.removeResourceChangeListener(listener);
if (charsetListener != null)
charsetListener.shutdown();
}
public void startup(IProgressMonitor monitor) {
job= new CharsetManagerJob();
listener= new Listener();
workspace.addResourceChangeListener(listener, IResourceChangeEvent.POST_CHANGE);
charsetListener= new CharsetDeltaJob(workspace);
charsetListener.startup();
}
}