/******************************************************************************* * Copyright (c) 2000, 2018 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 * which accompanies this distribution, and is available at * https://www.eclipse.org/legal/epl-2.0/ * * SPDX-License-Identifier: EPL-2.0 * * Contributors: * IBM Corporation - initial API and implementation * Axel Richard (Obeo) - Bug 41353 - Launch configurations prototypes *******************************************************************************/ package org.eclipse.debug.internal.core; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.eclipse.core.filesystem.EFS; import org.eclipse.core.filesystem.IFileStore; import org.eclipse.core.resources.IContainer; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.debug.core.DebugException; import org.eclipse.debug.core.DebugPlugin; import org.eclipse.debug.core.ILaunchConfiguration; import org.eclipse.debug.core.ILaunchConfigurationType; import org.eclipse.debug.core.ILaunchConfigurationWorkingCopy; import com.ibm.icu.text.MessageFormat; /** * A working copy launch configuration */ public class LaunchConfigurationWorkingCopy extends LaunchConfiguration implements ILaunchConfigurationWorkingCopy { /** * Handle of original launch configuration this * working copy is based on */ private LaunchConfiguration fOriginal; /** * Handle to a parent working copy * @since 3.3 */ private LaunchConfigurationWorkingCopy fParent = null; /** * Working copy of attributes. */ private LaunchConfigurationInfo fInfo; /** * Whether this working copy has been modified since * it was created */ private boolean fDirty; /** * Indicates whether this working copy has been explicitly renamed. */ private boolean fRenamed; /** * Suppress change notification until created */ private boolean fSuppressChange ; /** * Constructs a working copy of the specified launch * configuration. * * @param original launch configuration to make * a working copy of * @exception CoreException if unable to initialize this * working copy's attributes based on the original configuration */ protected LaunchConfigurationWorkingCopy(LaunchConfiguration original) throws CoreException { super(original.getName(), original.getContainer(), original.isPrototype()); copyFrom(original); setOriginal(original); fSuppressChange = false; } @Override protected void initialize() { fDirty = false; fRenamed = false; fSuppressChange = true; super.initialize(); } /** * Constructs a working copy of the specified launch configuration as its parent. * * @param parent launch configuration to make * a working copy of * @exception CoreException if unable to initialize this * working copy's attributes based on the original configuration */ protected LaunchConfigurationWorkingCopy(LaunchConfigurationWorkingCopy parent) throws CoreException { super(parent.getName(), parent.getContainer(), parent.isPrototype()); copyFrom(parent); setOriginal((LaunchConfiguration) parent.getOriginal()); fParent = parent; fSuppressChange = false; } /** * Constructs a copy of the specified launch * configuration, with the given (new) name. * * @param original launch configuration to make * a working copy of * @param name the new name for the copy of the launch * configuration * @exception CoreException if unable to initialize this * working copy's attributes based on the original configuration */ protected LaunchConfigurationWorkingCopy(LaunchConfiguration original, String name) throws CoreException { super(name, original.getContainer(), original.isPrototype()); copyFrom(original); fSuppressChange = false; } /** * Constructs a new working copy to be created in the specified * location. * * @param container the container that the configuration will be created in * or null if to be local * @param name the name of the new launch configuration * @param type the type of this working copy */ protected LaunchConfigurationWorkingCopy(IContainer container, String name, ILaunchConfigurationType type) { this(container, name, type, false); } /** * Constructs a new working copy to be created in the specified * location. * * @param container the container that the configuration will be created in * or null if to be local * @param name the name of the new launch configuration * @param type the type of this working copy * @param prototype if this copy is a prototype or not * * @since 3.12 */ protected LaunchConfigurationWorkingCopy(IContainer container, String name, ILaunchConfigurationType type, boolean prototype) { super(name, container, prototype); setInfo(new LaunchConfigurationInfo()); getInfo().setType(type); getInfo().setIsPrototype(prototype); fSuppressChange = false; } @Override public boolean isDirty() { return fDirty; } @Override public synchronized ILaunchConfiguration doSave() throws CoreException { return doSave(new NullProgressMonitor()); } /** * Saves with progress. * * @param monitor the {@link IProgressMonitor} * @return the saved ILaunchConfiguration * @throws CoreException if a problem is encountered * * @since 3.3 */ public synchronized ILaunchConfiguration doSave(IProgressMonitor monitor) throws CoreException { SubMonitor lmonitor = SubMonitor.convert(monitor, 1); try { if (getParent() != null) { // save to parent working copy LaunchConfigurationWorkingCopy wc = (LaunchConfigurationWorkingCopy) getParent(); if(isMoved()) { wc.rename(getName()); wc.setContainer(getContainer()); } wc.setAttributes(getInfo().getAttributes()); updateMonitor(lmonitor, 1); return wc; } else { boolean useRunnable= true; if (isLocal()) { if (isMoved()) { // If this config was moved from a shared location, saving // it will delete the original from the workspace. Use runnable. useRunnable= !isNew() && !getOriginal().isLocal(); } else { useRunnable= false; } } if (useRunnable) { IWorkspaceRunnable wr = pm -> doSave0(pm); ResourcesPlugin.getWorkspace().run(wr, null, 0, lmonitor.newChild(1)); } else { //file is persisted in the metadata not the workspace doSave0(lmonitor.newChild(1)); } getLaunchManager().setMovedFromTo(null, null); } } finally { if(lmonitor != null) { lmonitor.done(); } } return new LaunchConfiguration(getName(), getContainer(), isPrototype()); } /** * Performs the actual saving of the launch configuration. * @param monitor the {@link IProgressMonitor} * @throws CoreException if a problem is encountered */ private void doSave0(IProgressMonitor monitor) throws CoreException { SubMonitor lmonitor = SubMonitor.convert(monitor, MessageFormat.format(DebugCoreMessages.LaunchConfigurationWorkingCopy_0, new Object[] { getName() }), 2); try { // set up from/to information if this is a move boolean moved = (!isNew() && isMoved()); if (moved) { ILaunchConfiguration to = new LaunchConfiguration(getName(), getContainer(), isPrototype()); ILaunchConfiguration from = getOriginal(); getLaunchManager().setMovedFromTo(from, to); } ILaunchConfiguration orig = getOriginal(); updateMonitor(lmonitor, 1); writeNewFile(lmonitor.newChild(1)); // delete the old file if this is not a new configuration // or the file was renamed/moved if (moved) { orig.delete(); } fDirty = false; } finally { if(lmonitor != null) { lmonitor.done(); } } } /** * Writes the new configuration information to a file. * @param monitor the {@link IProgressMonitor} * * @exception CoreException if writing the file fails */ protected void writeNewFile(IProgressMonitor monitor) throws CoreException { String xml = null; try { xml = getInfo().getAsXML(); } catch (Exception e) { throw new DebugException( new Status( IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugException.REQUEST_FAILED, MessageFormat.format(DebugCoreMessages.LaunchConfigurationWorkingCopy__0__occurred_generating_launch_configuration_XML__1, new Object[] { e.toString() }), null ) ); } SubMonitor lmonitor = SubMonitor.convert(monitor, IInternalDebugCoreConstants.EMPTY_STRING, 5); try { boolean added = false; if (isLocal()) { // use java.io to update configuration file try { lmonitor.subTask(DebugCoreMessages.LaunchConfigurationWorkingCopy_1); IFileStore file = getFileStore(); if (file == null) { throw new DebugException( new Status( IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugException.REQUEST_FAILED, DebugCoreMessages.LaunchConfigurationWorkingCopy_4, null ) ); } IFileStore dir = file.getParent(); dir.mkdir(EFS.SHALLOW, null); if (!file.fetchInfo().exists()) { added = true; updateMonitor(lmonitor, 1); } BufferedOutputStream stream = null; try { stream = new BufferedOutputStream(file.openOutputStream(EFS.NONE, null)); stream.write(xml.getBytes(StandardCharsets.UTF_8)); } finally { if(stream != null) { stream.close(); } } //notify file saved updateMonitor(lmonitor, 1); } catch (IOException ie) { lmonitor.done(); throw new DebugException( new Status( IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugException.REQUEST_FAILED, MessageFormat.format(DebugCoreMessages.LaunchConfigurationWorkingCopy__0__occurred_generating_launch_configuration_XML__1, new Object[] { ie.toString() }), null ) ); } } else { // use resource API to update configuration file IFile file = getFile(); if (file == null) { lmonitor.done(); throw new DebugException( new Status( IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugException.REQUEST_FAILED, DebugCoreMessages.LaunchConfigurationWorkingCopy_5, null )); } IContainer dir = file.getParent(); if (!dir.exists()) { lmonitor.done(); throw new DebugException( new Status( IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugException.REQUEST_FAILED, DebugCoreMessages.LaunchConfigurationWorkingCopy_Specified_container_for_launch_configuration_does_not_exist_2, null ) ); } ByteArrayInputStream stream = new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8)); SubMonitor smonitor = null; if (!file.exists()) { added = true; //create file input stream: work one unit in a sub monitor smonitor = lmonitor.newChild(1); smonitor.setTaskName(MessageFormat.format(DebugCoreMessages.LaunchConfigurationWorkingCopy_2, new Object[] { getName() })); file.create(stream, false, smonitor); } else { // validate edit if (file.isReadOnly()) { IStatus status = ResourcesPlugin.getWorkspace().validateEdit(new IFile[] {file}, null); if (!status.isOK()) { lmonitor.done(); throw new CoreException(status); } } //set the contents of the file: work 1 unit in a sub monitor smonitor = lmonitor.newChild(1); smonitor.setTaskName(MessageFormat.format(DebugCoreMessages.LaunchConfigurationWorkingCopy_3, new Object[] { getName() })); file.setContents(stream, true, false, smonitor); } } // notify of add/change for both local and shared configurations - see bug 288368 if (added) { getLaunchManager().launchConfigurationAdded(new LaunchConfiguration(getName(), getContainer(), isPrototype())); } else { getLaunchManager().launchConfigurationChanged(new LaunchConfiguration(getName(), getContainer(), isPrototype())); } } finally { if(lmonitor != null) { lmonitor.done(); } } } /** * Updates the given monitor with the given tick count and polls for cancellation. If the monitor * is cancelled an {@link OperationCanceledException} is thrown * @param monitor the {@link IProgressMonitor} * @param ticks the amount of work to advance the monitor * @throws OperationCanceledException if the user cancels the operation */ private void updateMonitor(IProgressMonitor monitor, int ticks) throws OperationCanceledException { if(monitor != null) { monitor.worked(ticks); if (monitor.isCanceled()) { throw new OperationCanceledException(); } } } @Override public void setAttribute(String attributeName, int value) { getInfo().setAttribute(attributeName, Integer.valueOf(value)); setDirty(); } @Override public void setAttribute(String attributeName, String value) { getInfo().setAttribute(attributeName, value); setDirty(); } @Override public void setAttribute(String attributeName, boolean value) { getInfo().setAttribute(attributeName, Boolean.valueOf(value)); setDirty(); } @Override public void setAttribute(String attributeName, List value) { getInfo().setAttribute(attributeName, value); setDirty(); } @Override public void setAttribute(String attributeName, Map value) { getInfo().setAttribute(attributeName, value); setDirty(); } @Override public void setAttribute(String attributeName, Set value) { getInfo().setAttribute(attributeName, value); setDirty(); } @Override public void setAttribute(String attributeName, Object value) { getInfo().setAttribute(attributeName, value); setDirty(); } @Override public ILaunchConfiguration getOriginal() { ILaunchConfiguration config = fOriginal; ILaunchConfigurationWorkingCopy parent = fParent; while(parent != null) { config = parent.getOriginal(); parent = parent.getParent(); } return config; } @Override public ILaunchConfigurationWorkingCopy getParent() { return fParent; } /** * Sets the launch configuration this working copy * is based on. Initializes the attributes of this * working copy to the current values of the given * configuration. * * @param original the launch configuration this working * copy is based on. * @exception CoreException if unable to initialize this * working copy based on the original's current attribute * set */ private void copyFrom(LaunchConfiguration original) throws CoreException { LaunchConfigurationInfo info = original.getInfo(); setInfo(info.getCopy()); fDirty = false; } /** * Sets the launch configuration this working copy * is based on. * * @param original the launch configuration this working * copy is based on. */ private void setOriginal(LaunchConfiguration original) { fOriginal = original; } /** * Sets the working copy info object for this working copy. * * @param info a copy of attributes from this working copy's * original launch configuration */ protected void setInfo(LaunchConfigurationInfo info) { fInfo = info; } @Override public boolean isWorkingCopy() { return true; } /** * A working copy keeps a local info object that is not * cached with the launch manager. * * @see LaunchConfiguration#getInfo() */ @Override protected LaunchConfigurationInfo getInfo() { return fInfo; } /** * Sets this working copy's state to dirty. * Notifies listeners that this working copy has * changed. */ private void setDirty() { fDirty = true; if (!suppressChangeNotification()) { getLaunchManager().getConfigurationNotifier().notify(this, LaunchManager.CHANGED); } } @Override public void setModes(Set modes) { getInfo().setAttribute(ATTR_LAUNCH_MODES, (modes.size() > 0 ? modes : null)); setDirty(); } /** * @see org.eclipse.debug.core.ILaunchConfigurationWorkingCopy#addModes(java.util.Set) */ @Override public void addModes(Set modes) { try { Set opts = getModes(); if(opts.addAll(modes)) { getInfo().setAttribute(ATTR_LAUNCH_MODES, opts); setDirty(); } } catch (CoreException e) { DebugPlugin.log(e); } } /** * @see org.eclipse.debug.core.ILaunchConfigurationWorkingCopy#removeModes(java.util.Set) */ @Override public void removeModes(Set options) { try { Set opts = getModes(); if(opts.removeAll(options)) { getInfo().setAttribute(ATTR_LAUNCH_MODES, (opts.size() < 1 ? null : opts)); setDirty(); } } catch (CoreException e) { DebugPlugin.log(e); } } /** * @see ILaunchConfigurationWorkingCopy#rename(String) */ @Override public void rename(String name) { if (!getName().equals(name)) { setName(name); fRenamed = isNew() || !(getOriginal().getName().equals(name)); } } /** * Sets the new name for this configuration. * * @param name the new name for this configuration */ @Override protected void setName(String name) { super.setName(name); setDirty(); } /** * Returns whether this working copy is new, or is a * working copy of another launch configuration. * * @return whether this working copy is new, or is a * working copy of another launch configuration */ protected boolean isNew() { return getOriginal() == null; } /** * Returns whether this working copy is new or if its * location has changed from that of its original. * * @return whether this working copy is new or if its * location has changed from that of its original */ protected boolean isMoved() { if (isNew() || fRenamed) { return true; } IContainer newContainer = getContainer(); IContainer originalContainer = ((LaunchConfiguration)getOriginal()).getContainer(); if (newContainer == originalContainer) { return false; } if (newContainer == null) { return !originalContainer.equals(newContainer); } return !newContainer.equals(originalContainer); } /** * A working copy cannot generate a memento. * * @see ILaunchConfiguration#getMemento() */ @Override public String getMemento() { return null; } /** * Returns whether change notification should be * suppressed * @return if changes notification should be suppressed */ protected boolean suppressChangeNotification() { return fSuppressChange; } @Override public void setContainer(IContainer container) { if (equalOrNull(getContainer(), container)) { return; } super.setContainer(container); setDirty(); } @Override public void setAttributes(Map attributes) { getInfo().setAttributes(attributes); setDirty(); } @Override public void setMappedResources(IResource[] resources) { ArrayList paths = null; ArrayList types = null; if(resources != null && resources.length > 0) { paths = new ArrayList<>(resources.length); types = new ArrayList<>(resources.length); for (int i = 0; i < resources.length; i++) { IResource resource = resources[i]; if(resource != null) { paths.add(resource.getFullPath().toPortableString()); types.add(Integer.valueOf(resource.getType()).toString()); } } } setAttribute(LaunchConfiguration.ATTR_MAPPED_RESOURCE_PATHS, paths); setAttribute(LaunchConfiguration.ATTR_MAPPED_RESOURCE_TYPES, types); } @Override public void setPreferredLaunchDelegate(Set modes, String delegateId) { if(modes != null) { try { Map delegates = getAttribute(LaunchConfiguration.ATTR_PREFERRED_LAUNCHERS, (Map) null); //copy map to avoid pointer issues Map map = new HashMap<>(); if (delegates != null) { map.putAll(delegates); } if (delegateId == null) { map.remove(modes.toString()); } else { map.put(modes.toString(), delegateId); } setAttribute(LaunchConfiguration.ATTR_PREFERRED_LAUNCHERS, map); } catch (CoreException ce) {DebugPlugin.log(ce);} } } @Override public ILaunchConfigurationWorkingCopy getWorkingCopy() throws CoreException { return new LaunchConfigurationWorkingCopy(this); } @Override public Object removeAttribute(String attributeName) { return getInfo().removeAttribute(attributeName); } @Override public void copyAttributes(ILaunchConfiguration prototype) throws CoreException { Map map = prototype.getAttributes(); LaunchConfigurationInfo info = getInfo(); info.setPrototype(prototype); Set prototypeVisibleAttributes = prototype.getPrototypeVisibleAttributes(); if (prototypeVisibleAttributes != null) { prototypeVisibleAttributes.forEach(key -> { Object value = map.get(key); if (value != null) { info.setAttribute(key, value); } }); } } @Override public void setPrototype(ILaunchConfiguration prototype, boolean copy) throws CoreException { if (prototype != null && !prototype.isPrototype()) { throw new CoreException(new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugCoreMessages.LaunchConfigurationWorkingCopy_6)); } if (prototype != null && prototype.isWorkingCopy()) { throw new CoreException(new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugCoreMessages.LaunchConfigurationWorkingCopy_7)); } if (prototype == null) { getInfo().setPrototype(null); removeAttribute(ATTR_PROTOTYPE); } else { if (isPrototype()) { throw new CoreException(new Status(IStatus.ERROR, DebugPlugin.getUniqueIdentifier(), DebugCoreMessages.LaunchConfigurationWorkingCopy_8)); } getInfo().setPrototype(prototype); if (copy) { copyAttributes(prototype); } setAttribute(ATTR_PROTOTYPE, prototype.getMemento()); setAttribute(IS_PROTOTYPE, false); } } @Override public ILaunchConfiguration doSave(int flag) throws CoreException { Collection children = null; if (UPDATE_PROTOTYPE_CHILDREN == flag) { if (!isNew() && isMoved() && getParent() == null) { children = getOriginal().getPrototypeChildren(); } } ILaunchConfiguration saved = doSave(); if (children != null) { for (ILaunchConfiguration child : children) { ILaunchConfigurationWorkingCopy wc = child.getWorkingCopy(); wc.setPrototype(saved, false); wc.doSave(); } } return saved; } @Override public void setPrototypeAttributeVisibility(String attribute, boolean visible) throws CoreException { super.setPrototypeAttributeVisibility(attribute, visible); setDirty(); } }