/**
   * Returns <code>ITextSelection.class</code> or <code>null</code> if the class is not available.
   *
   * @return <code>ITextSelection.class</code> or <code>null</code> if class not available
   * @since 1.0
   */
  private static Class getTextSelectionClass() {
    if (iTextSelectionClass != null) {
      // tried before and succeeded
      return iTextSelectionClass;
    }
    if (!textSelectionPossible) {
      // tried before and failed
      return null;
    }

    // JFace text plug-in is not on prereq chain of generic wb plug-in
    // hence: ITextSelection.class won't compile
    // and Class.forName("org.eclipse.jface.text.ITextSelection") won't find
    // it need to be trickier...
    Bundle bundle = Platform.getBundle(JFACE_TEXT_PLUG_IN);
    if (bundle == null || bundle.getState() == Bundle.UNINSTALLED) {
      // JFace text plug-in is not around, or has already
      // been removed, assume that it will never be around
      textSelectionPossible = false;
      return null;
    }

    // plug-in is around
    // it's not our job to activate the plug-in
    if (bundle.getState() == Bundle.INSTALLED) {
      // assume it might come alive later
      textSelectionPossible = true;
      return null;
    }

    try {
      Class c = bundle.loadClass(TEXT_SELECTION_CLASS);
      // remember for next time
      iTextSelectionClass = c;
      return iTextSelectionClass;
    } catch (ClassNotFoundException e) {
      // unable to load ITextSelection - sounds pretty serious
      // treat as if JFace text plug-in were unavailable
      textSelectionPossible = false;
      return null;
    }
  }
Example #2
0
 protected ResourceBundle getPluginResourceBundle() {
   Bundle bundle = Platform.getBundle(Activator.getPluginId());
   return Platform.getResourceBundle(bundle);
 }
/**
 * 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<Map.Entry<IProject, Boolean>> asyncChanges = new ArrayList<>();

    public CharsetManagerJob() {
      super(Messages.resources_charsetUpdating);
      setSystem(true);
      setPriority(Job.INTERACTIVE);
    }

    public void addChanges(Map<IProject, Boolean> newChanges) {
      if (newChanges.isEmpty()) return;
      synchronized (asyncChanges) {
        asyncChanges.addAll(newChanges.entrySet());
        asyncChanges.notify();
      }
      schedule(CHARSET_UPDATE_DELAY);
    }

    public Map.Entry<IProject, Boolean> getNextChange() {
      synchronized (asyncChanges) {
        return asyncChanges.isEmpty() ? null : asyncChanges.remove(asyncChanges.size() - 1);
      }
    }

    @Override
    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);
          Map.Entry<IProject, Boolean> 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;
            IProject project = next.getKey();
            try {
              if (project.isAccessible()) {
                boolean shouldDisableCharsetDeltaJob = next.getValue().booleanValue();
                // flush preferences for non-derived resources
                flushPreferences(
                    getPreferences(project, false, false, true), shouldDisableCharsetDeltaJob);
                // flush preferences for derived resources
                flushPreferences(
                    getPreferences(project, false, true, true), shouldDisableCharsetDeltaJob);
              }
            } catch (BackingStoreException e) {
              // we got an error saving
              String detailMessage = Messages.resources_savingEncoding;
              result.add(
                  new ResourceStatus(
                      IResourceStatus.FAILED_SETTING_CHARSET,
                      project.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;
    }

    @Override
    public boolean shouldRun() {
      synchronized (asyncChanges) {
        return !asyncChanges.isEmpty();
      }
    }
  }

  private class ResourceChangeListener implements IResourceChangeListener {
    public ResourceChangeListener() {}

    private boolean moveSettingsIfDerivedChanged(
        IResourceDelta parent,
        IProject currentProject,
        Preferences projectPrefs,
        String[] affectedResources) {
      boolean resourceChanges = false;

      if ((parent.getFlags() & IResourceDelta.DERIVED_CHANGED) != 0) {
        // if derived changed, move encoding to correct preferences
        IPath parentPath = parent.getResource().getProjectRelativePath();
        for (int i = 0; i < affectedResources.length; i++) {
          IPath affectedPath = new Path(affectedResources[i]);
          // if parentPath is an ancestor of affectedPath
          if (parentPath.isPrefixOf(affectedPath)) {
            IResource member = currentProject.findMember(affectedPath);
            if (member != null) {
              Preferences targetPrefs =
                  getPreferences(currentProject, true, member.isDerived(IResource.CHECK_ANCESTORS));
              // if new preferences are different than current
              if (!projectPrefs.absolutePath().equals(targetPrefs.absolutePath())) {
                // remove encoding from old preferences and save in correct preferences
                String currentValue = projectPrefs.get(affectedResources[i], null);
                projectPrefs.remove(affectedResources[i]);
                targetPrefs.put(affectedResources[i], currentValue);
                resourceChanges = true;
              }
            }
          }
        }
      }

      IResourceDelta[] children = parent.getAffectedChildren();
      for (int i = 0; i < children.length; i++) {
        resourceChanges =
            moveSettingsIfDerivedChanged(
                    children[i], currentProject, projectPrefs, affectedResources)
                || resourceChanges;
      }
      return resourceChanges;
    }

    private void processEntryChanges(
        IResourceDelta projectDelta, Map<IProject, Boolean> projectsToSave) {
      // check each resource with user-set encoding to see if it has
      // been moved/deleted or if derived state has been changed
      IProject currentProject = (IProject) projectDelta.getResource();
      Preferences projectRegularPrefs = getPreferences(currentProject, false, false, true);
      Preferences projectDerivedPrefs = getPreferences(currentProject, false, true, true);
      Map<Boolean, String[]> affectedResourcesMap = new HashMap<>();
      try {
        // no regular preferences for this project
        if (projectRegularPrefs == null) affectedResourcesMap.put(Boolean.FALSE, new String[0]);
        else affectedResourcesMap.put(Boolean.FALSE, projectRegularPrefs.keys());
        // no derived preferences for this project
        if (projectDerivedPrefs == null) affectedResourcesMap.put(Boolean.TRUE, new String[0]);
        else affectedResourcesMap.put(Boolean.TRUE, projectDerivedPrefs.keys());
      } catch (BackingStoreException e) {
        // problems with the project scope... we will 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 (Iterator<Boolean> it = affectedResourcesMap.keySet().iterator(); it.hasNext(); ) {
        Boolean isDerived = it.next();
        String[] affectedResources = affectedResourcesMap.get(isDerived);
        Preferences projectPrefs =
            isDerived.booleanValue() ? projectDerivedPrefs : projectRegularPrefs;
        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) {
            boolean shouldDisableCharsetDeltaJobForCurrentProject = false;
            // 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) {
              IPath movedToPath = memberDelta.getMovedToPath();
              IResource resource = workspace.getRoot().findMember(movedToPath);
              if (resource != null) {
                Preferences encodingSettings =
                    getPreferences(
                        resource.getProject(), true, resource.isDerived(IResource.CHECK_ANCESTORS));
                if (currentValue == null || currentValue.trim().length() == 0)
                  encodingSettings.remove(getKeyFor(movedToPath));
                else encodingSettings.put(getKeyFor(movedToPath), currentValue);
                IProject targetProject = workspace.getRoot().getProject(movedToPath.segment(0));
                if (targetProject.equals(currentProject))
                  // if the file was moved inside the same project disable charset listener
                  shouldDisableCharsetDeltaJobForCurrentProject = true;
                else projectsToSave.put(targetProject, Boolean.FALSE);
              }
            }
            projectsToSave.put(
                currentProject, Boolean.valueOf(shouldDisableCharsetDeltaJobForCurrentProject));
          }
        }
        if (moveSettingsIfDerivedChanged(
            projectDelta, currentProject, projectPrefs, affectedResources)) {
          // if settings were moved between preferences files disable charset listener so we don't
          // react to changes made by ourselves
          projectsToSave.put(currentProject, Boolean.TRUE);
        }
      }
    }

    /**
     * For any change to the encoding file or any resource with encoding set, just discard the cache
     * for the corresponding project.
     */
    @Override
    public void resourceChanged(IResourceChangeEvent event) {
      IResourceDelta delta = event.getDelta();
      if (delta == null) return;
      IResourceDelta[] projectDeltas = delta.getAffectedChildren();
      // process each project in the delta
      Map<IProject, Boolean> projectsToSave = new HashMap<>();
      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);
    }
  }

  private static final String PROJECT_KEY = "<project>"; // $NON-NLS-1$
  private CharsetDeltaJob charsetListener;
  CharsetManagerJob job;
  private IResourceChangeListener resourceChangeListener;
  protected final Bundle systemBundle = Platform.getBundle("org.eclipse.osgi"); // $NON-NLS-1$
  Workspace workspace;

  public CharsetManager(Workspace workspace) {
    this.workspace = workspace;
  }

  void flushPreferences(Preferences projectPrefs, boolean shouldDisableCharsetDeltaJob)
      throws BackingStoreException {
    if (projectPrefs != null) {
      try {
        if (shouldDisableCharsetDeltaJob) charsetListener.setDisabled(true);
        projectPrefs.flush();
      } finally {
        if (shouldDisableCharsetDeltaJob) charsetListener.setDisabled(false);
      }
    }
  }

  /**
   * 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 prefs = getPreferences(project, false, false);
    Preferences derivedPrefs = getPreferences(project, false, true);

    if (prefs == null && derivedPrefs == 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(prefs, derivedPrefs, resourcePath, recurse);
  }

  static String getKeyFor(IPath resourcePath) {
    return resourcePath.segmentCount() > 1
        ? resourcePath.removeFirstSegments(1).toString()
        : PROJECT_KEY;
  }

  Preferences getPreferences(IProject project, boolean create, boolean isDerived) {
    return getPreferences(project, create, isDerived, isDerivedEncodingStoredSeparately(project));
  }

  Preferences getPreferences(
      IProject project,
      boolean create,
      boolean isDerived,
      boolean isDerivedEncodingStoredSeparately) {
    boolean localIsDerived = isDerivedEncodingStoredSeparately ? isDerived : false;
    String qualifier =
        localIsDerived
            ? ProjectPreferences.PREFS_DERIVED_QUALIFIER
            : ProjectPreferences.PREFS_REGULAR_QUALIFIER;
    if (create)
      // create all nodes down to the one we are interested in
      return new ProjectScope(project).getNode(qualifier).node(ResourcesPlugin.PREF_ENCODING);
    // 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(qualifier)) return null;
      node = node.node(qualifier);
      if (!node.nodeExists(ResourcesPlugin.PREF_ENCODING)) return null;
      return node.node(ResourcesPlugin.PREF_ENCODING);
    } 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(
      Preferences prefs, Preferences derivedPrefs, IPath resourcePath, boolean recurse) {
    String charset = null;

    // try to find the encoding in regular and then derived preferences
    if (prefs != null) charset = prefs.get(getKeyFor(resourcePath), null);
    // derivedPrefs may be not null, only if #isDerivedEncodingStoredSeparately returns true
    // so the explicit check against #isDerivedEncodingStoredSeparately is not required
    if (charset == null && derivedPrefs != null)
      charset = derivedPrefs.get(getKeyFor(resourcePath), null);

    if (!recurse) return charset;

    while (charset == null && resourcePath.segmentCount() > 1) {
      resourcePath = resourcePath.removeLastSegments(1);
      // try to find the encoding in regular and then derived preferences
      if (prefs != null) charset = prefs.get(getKeyFor(resourcePath), null);
      if (charset == null && derivedPrefs != null)
        charset = derivedPrefs.get(getKeyFor(resourcePath), null);
    }

    // ensure we default to the workspace encoding if none is found
    return charset == null ? ResourcesPlugin.getEncoding() : charset;
  }

  private boolean isDerivedEncodingStoredSeparately(IProject project) {
    // 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;
      //			return node.nodeExists(path) ?
      // node.node(path).getBoolean(ResourcesPlugin.PREF_SEPARATE_DERIVED_ENCODINGS, false) : false;
      // for now, take the long way
      if (!node.nodeExists(project.getName()))
        return ResourcesPlugin.DEFAULT_PREF_SEPARATE_DERIVED_ENCODINGS;
      node = node.node(project.getName());
      if (!node.nodeExists(ResourcesPlugin.PI_RESOURCES))
        return ResourcesPlugin.DEFAULT_PREF_SEPARATE_DERIVED_ENCODINGS;
      node = node.node(ResourcesPlugin.PI_RESOURCES);
      return node.getBoolean(
          ResourcesPlugin.PREF_SEPARATE_DERIVED_ENCODINGS,
          ResourcesPlugin.DEFAULT_PREF_SEPARATE_DERIVED_ENCODINGS);
    } catch (BackingStoreException e) {
      // nodeExists failed
      String message = Messages.resources_readingEncoding;
      Policy.log(
          new ResourceStatus(
              IResourceStatus.FAILED_GETTING_CHARSET, project.getFullPath(), message, e));
      return ResourcesPlugin.DEFAULT_PREF_SEPARATE_DERIVED_ENCODINGS;
    }
  }

  protected void mergeEncodingPreferences(IProject project) {
    Preferences projectRegularPrefs = null;
    Preferences projectDerivedPrefs = getPreferences(project, false, true, true);
    if (projectDerivedPrefs == null) return;
    try {
      boolean prefsChanged = false;
      String[] affectedResources;
      affectedResources = projectDerivedPrefs.keys();
      for (int i = 0; i < affectedResources.length; i++) {
        String path = affectedResources[i];
        String value = projectDerivedPrefs.get(path, null);
        projectDerivedPrefs.remove(path);
        // lazy creation of non-derived preferences
        if (projectRegularPrefs == null)
          projectRegularPrefs = getPreferences(project, true, false, false);
        projectRegularPrefs.put(path, value);
        prefsChanged = true;
      }
      if (prefsChanged) {
        Map<IProject, Boolean> projectsToSave = new HashMap<>();
        // this is internal change so do not notify charset delta job
        projectsToSave.put(project, Boolean.TRUE);
        job.addChanges(projectsToSave);
      }
    } catch (BackingStoreException e) {
      // problems with the project scope... we will miss the changes (but will log)
      String message = Messages.resources_readingEncoding;
      Policy.log(
          new ResourceStatus(
              IResourceStatus.FAILED_GETTING_CHARSET, project.getFullPath(), message, e));
    }
  }

  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) {
      IEclipsePreferences resourcesPreferences =
          InstanceScope.INSTANCE.getNode(ResourcesPlugin.PI_RESOURCES);
      if (newCharset != null) resourcesPreferences.put(ResourcesPlugin.PREF_ENCODING, newCharset);
      else resourcesPreferences.remove(ResourcesPlugin.PREF_ENCODING);
      try {
        resourcesPreferences.flush();
      } catch (BackingStoreException e) {
        IProject project = workspace.getRoot().getProject(resourcePath.segment(0));
        String message = Messages.resources_savingEncoding;
        throw new ResourceException(
            IResourceStatus.FAILED_SETTING_CHARSET, project.getFullPath(), message, e);
      }
      return;
    }
    // for all other cases, we set a property in the corresponding project
    IResource resource = workspace.getRoot().findMember(resourcePath);
    if (resource != null) {
      try {
        // disable the listener so we don't react to changes made by ourselves
        Preferences encodingSettings =
            getPreferences(
                resource.getProject(), true, resource.isDerived(IResource.CHECK_ANCESTORS));
        if (newCharset == null || newCharset.trim().length() == 0)
          encodingSettings.remove(getKeyFor(resourcePath));
        else encodingSettings.put(getKeyFor(resourcePath), newCharset);
        flushPreferences(encodingSettings, true);
      } catch (BackingStoreException e) {
        IProject project = workspace.getRoot().getProject(resourcePath.segment(0));
        String message = Messages.resources_savingEncoding;
        throw new ResourceException(
            IResourceStatus.FAILED_SETTING_CHARSET, project.getFullPath(), message, e);
      }
    }
  }

  @Override
  public void shutdown(IProgressMonitor monitor) {
    workspace.removeResourceChangeListener(resourceChangeListener);
    if (charsetListener != null) charsetListener.shutdown();
  }

  protected void splitEncodingPreferences(IProject project) {
    Preferences projectRegularPrefs = getPreferences(project, false, false, false);
    Preferences projectDerivedPrefs = null;
    if (projectRegularPrefs == null) return;
    try {
      boolean prefsChanged = false;
      String[] affectedResources;
      affectedResources = projectRegularPrefs.keys();
      for (int i = 0; i < affectedResources.length; i++) {
        String path = affectedResources[i];
        IResource resource = project.findMember(path);
        if (resource != null) {
          if (resource.isDerived(IResource.CHECK_ANCESTORS)) {
            String value = projectRegularPrefs.get(path, null);
            projectRegularPrefs.remove(path);
            // lazy creation of derived preferences
            if (projectDerivedPrefs == null)
              projectDerivedPrefs = getPreferences(project, true, true, true);
            projectDerivedPrefs.put(path, value);
            prefsChanged = true;
          }
        }
      }
      if (prefsChanged) {
        Map<IProject, Boolean> projectsToSave = new HashMap<>();
        // this is internal change so do not notify charset delta job
        projectsToSave.put(project, Boolean.TRUE);
        job.addChanges(projectsToSave);
      }
    } catch (BackingStoreException e) {
      // problems with the project scope... we will miss the changes (but will log)
      String message = Messages.resources_readingEncoding;
      Policy.log(
          new ResourceStatus(
              IResourceStatus.FAILED_GETTING_CHARSET, project.getFullPath(), message, e));
    }
  }

  @Override
  public void startup(IProgressMonitor monitor) {
    job = new CharsetManagerJob();
    resourceChangeListener = new ResourceChangeListener();
    workspace.addResourceChangeListener(resourceChangeListener, IResourceChangeEvent.POST_CHANGE);
    charsetListener = new CharsetDeltaJob(workspace);
    charsetListener.startup();
  }
}