001: /*******************************************************************************
002: * Copyright (c) 2004, 2006 IBM Corporation and others.
003: * All rights reserved. This program and the accompanying materials
004: * are made available under the terms of the Eclipse Public License v1.0
005: * which accompanies this distribution, and is available at
006: * http://www.eclipse.org/legal/epl-v10.html
007: *
008: * Contributors:
009: * IBM Corporation - initial API and implementation
010: *******************************************************************************/package org.eclipse.ui.internal.ide;
011:
012: import java.io.File;
013: import java.io.FileReader;
014: import java.io.IOException;
015: import java.io.Reader;
016: import java.net.URL;
017: import java.util.StringTokenizer;
018:
019: import org.eclipse.core.runtime.Path;
020: import org.eclipse.core.runtime.Platform;
021: import org.eclipse.core.runtime.preferences.ConfigurationScope;
022: import org.eclipse.jface.preference.IPreferenceStore;
023: import org.eclipse.osgi.service.datalocation.Location;
024: import org.eclipse.ui.IMemento;
025: import org.eclipse.ui.WorkbenchException;
026: import org.eclipse.ui.XMLMemento;
027: import org.eclipse.ui.ide.IDE;
028: import org.eclipse.ui.preferences.ScopedPreferenceStore;
029: import org.osgi.service.prefs.BackingStoreException;
030: import org.osgi.service.prefs.Preferences;
031:
032: /**
033: * This class stores the information behind the "Launch Workspace" dialog. The
034: * class is able to read and write itself to a well known configuration file.
035: */
036: public class ChooseWorkspaceData {
037: /**
038: * The default max length of the recent workspace mru list.
039: */
040: private static final int RECENT_MAX_LENGTH = 5;
041:
042: /**
043: * The directory within the config area that will be used for the
044: * receiver's persisted data.
045: */
046: private static final String PERS_FOLDER = "org.eclipse.ui.ide"; //$NON-NLS-1$
047:
048: /**
049: * The name of the file within the config area that will be used for
050: * the recever's persisted data.
051: * @see PERS_FOLDER
052: */
053: private static final String PERS_FILENAME = "recentWorkspaces.xml"; //$NON-NLS-1$
054:
055: /**
056: * In the past a file was used to store persist these values. This file was written
057: * with this value as its protocol identifier.
058: */
059: private static final int PERS_ENCODING_VERSION = 1;
060:
061: /**
062: * This is the first version of the encode/decode protocol that uses the config area
063: * preference store for persistence. The only encoding done is to convert the recent
064: * workspace list into a comma-separated list.
065: */
066: private static final int PERS_ENCODING_VERSION_CONFIG_PREFS = 2;
067:
068: /**
069: * This is the second version of the encode/decode protocol that uses the
070: * confi area preferences store for persistence. This version is the same as
071: * the previous version except it uses a \n character to seperate the path
072: * entries instead of commas. (see bug 98467)
073: *
074: * @since 3.3.1
075: */
076: private static final int PERS_ENCODING_VERSION_CONFIG_PREFS_NO_COMMAS = 3;
077:
078: private boolean showDialog = true;
079:
080: private String initialDefault;
081:
082: private String selection;
083:
084: private String[] recentWorkspaces;
085:
086: // xml tags
087: private static interface XML {
088: public static final String PROTOCOL = "protocol"; //$NON-NLS-1$
089:
090: public static final String VERSION = "version"; //$NON-NLS-1$
091:
092: public static final String ALWAYS_ASK = "alwaysAsk"; //$NON-NLS-1$
093:
094: public static final String SHOW_DIALOG = "showDialog"; //$NON-NLS-1$
095:
096: public static final String WORKSPACE = "workspace"; //$NON-NLS-1$
097:
098: public static final String RECENT_WORKSPACES = "recentWorkspaces"; //$NON-NLS-1$
099:
100: public static final String MAX_LENGTH = "maxLength"; //$NON-NLS-1$
101:
102: public static final String PATH = "path"; //$NON-NLS-1$
103: }
104:
105: /**
106: * Creates a new instance, loading persistent data if its found.
107: */
108: public ChooseWorkspaceData(String initialDefault) {
109: readPersistedData();
110: setInitialDefault(initialDefault);
111: }
112:
113: /**
114: * Creates a new instance, loading persistent data if its found.
115: */
116: public ChooseWorkspaceData(URL instanceUrl) {
117: readPersistedData();
118: if (instanceUrl != null) {
119: setInitialDefault(new File(instanceUrl.getFile())
120: .toString());
121: }
122: }
123:
124: /**
125: * Return the folder to be used as a default if no other information
126: * exists. Does not return null.
127: */
128: public String getInitialDefault() {
129: if (initialDefault == null) {
130: setInitialDefault(System.getProperty("user.dir") //$NON-NLS-1$
131: + File.separator + "workspace"); //$NON-NLS-1$
132: }
133: return initialDefault;
134: }
135:
136: /**
137: * Set this data's initialDefault parameter to a properly formatted version
138: * of the argument directory string. The proper format is to the platform
139: * appropriate separator character without meaningless leading or trailing
140: * separator characters.
141: */
142: private void setInitialDefault(String dir) {
143: if (dir == null || dir.length() <= 0) {
144: initialDefault = null;
145: return;
146: }
147:
148: dir = new Path(dir).toOSString();
149: while (dir.charAt(dir.length() - 1) == File.separatorChar) {
150: dir = dir.substring(0, dir.length() - 1);
151: }
152: initialDefault = dir;
153: }
154:
155: /**
156: * Return the currently selected workspace or null if nothing is selected.
157: */
158: public String getSelection() {
159: return selection;
160: }
161:
162: /**
163: * Return the currently selected workspace or null if nothing is selected.
164: */
165: public boolean getShowDialog() {
166: return showDialog;
167: }
168:
169: /**
170: * Return an array of recent workspaces sorted with the most recently used at
171: * the start.
172: */
173: public String[] getRecentWorkspaces() {
174: return recentWorkspaces;
175: }
176:
177: /**
178: * The argument workspace has been selected, update the receiver. Does not
179: * persist the new values.
180: */
181: public void workspaceSelected(String dir) {
182: // this just stores the selection, it is not inserted and persisted
183: // until the workspace is actually selected
184: selection = dir;
185: }
186:
187: /**
188: * Toggle value of the showDialog persistent setting.
189: */
190: public void toggleShowDialog() {
191: showDialog = !showDialog;
192: }
193:
194: /**
195: * Update the persistent store. Call this function after the currently
196: * selected value has been found to be ok.
197: */
198: public void writePersistedData() {
199: // 1. get config pref node
200: Preferences node = new ConfigurationScope()
201: .getNode(IDEWorkbenchPlugin.IDE_WORKBENCH);
202:
203: // 2. get value for showDialog
204: node.putBoolean(
205: IDE.Preferences.SHOW_WORKSPACE_SELECTION_DIALOG,
206: showDialog);
207:
208: // 3. use value of numRecent to create proper length array
209: node.putInt(IDE.Preferences.MAX_RECENT_WORKSPACES,
210: recentWorkspaces.length);
211:
212: // move the new selection to the front of the list
213: if (selection != null) {
214: String oldEntry = recentWorkspaces[0];
215: recentWorkspaces[0] = selection;
216: for (int i = 1; i < recentWorkspaces.length
217: && oldEntry != null; ++i) {
218: if (selection.equals(oldEntry)) {
219: break;
220: }
221: String tmp = recentWorkspaces[i];
222: recentWorkspaces[i] = oldEntry;
223: oldEntry = tmp;
224: }
225: }
226:
227: // 4. store values of recent workspaces into array
228: String encodedRecentWorkspaces = encodeStoredWorkspacePaths(recentWorkspaces);
229: node.put(IDE.Preferences.RECENT_WORKSPACES,
230: encodedRecentWorkspaces);
231:
232: // 5. store the protocol version used to encode the list
233: node.putInt(IDE.Preferences.RECENT_WORKSPACES_PROTOCOL,
234: PERS_ENCODING_VERSION_CONFIG_PREFS_NO_COMMAS);
235:
236: // 6. store the node
237: try {
238: node.flush();
239: } catch (BackingStoreException e) {
240: // do nothing
241: }
242: }
243:
244: /**
245: * Look for and read data that might have been persisted from some previous
246: * run. Leave the receiver in a default state if no persistent data is
247: * found.
248: *
249: * @return true if a file was successfully read and false otherwise
250: */
251: private boolean readPersistedData_file() {
252: URL persUrl = null;
253:
254: Location configLoc = Platform.getConfigurationLocation();
255: if (configLoc != null) {
256: persUrl = getPersistenceUrl(configLoc.getURL(), false);
257: }
258:
259: try {
260: // inside try to get the safe default creation in the finally
261: // clause
262: if (persUrl == null) {
263: return false;
264: }
265:
266: // E.g.,
267: // <launchWorkspaceData>
268: // <protocol version="1"/>
269: // <alwaysAsk showDialog="1"/>
270: // <recentWorkspaces maxLength="5">
271: // <workspace path="C:\eclipse\workspace0"/>
272: // <workspace path="C:\eclipse\workspace1"/>
273: // </recentWorkspaces>
274: // </launchWorkspaceData>
275:
276: Reader reader = new FileReader(persUrl.getFile());
277: XMLMemento memento = XMLMemento.createReadRoot(reader);
278: if (memento == null || !compatibleFileProtocol(memento)) {
279: return false;
280: }
281:
282: IMemento alwaysAskTag = memento.getChild(XML.ALWAYS_ASK);
283: showDialog = alwaysAskTag == null ? true : alwaysAskTag
284: .getInteger(XML.SHOW_DIALOG).intValue() == 1;
285:
286: IMemento recent = memento.getChild(XML.RECENT_WORKSPACES);
287: if (recent == null) {
288: return false;
289: }
290:
291: Integer maxLength = recent.getInteger(XML.MAX_LENGTH);
292: int max = RECENT_MAX_LENGTH;
293: if (maxLength != null) {
294: max = maxLength.intValue();
295: }
296:
297: IMemento indices[] = recent.getChildren(XML.WORKSPACE);
298: if (indices == null || indices.length <= 0) {
299: return false;
300: }
301:
302: // if a user has edited maxLength to be shorter than the listed
303: // indices, accept the list (its tougher for them to retype a long
304: // list of paths than to update a max number)
305: max = Math.max(max, indices.length);
306:
307: recentWorkspaces = new String[max];
308: for (int i = 0; i < indices.length; ++i) {
309: String path = indices[i].getString(XML.PATH);
310: if (path == null) {
311: break;
312: }
313: recentWorkspaces[i] = path;
314: }
315: } catch (IOException e) {
316: // cannot log because instance area has not been set
317: return false;
318: } catch (WorkbenchException e) {
319: // cannot log because instance area has not been set
320: return false;
321: } finally {
322: // create safe default if needed
323: if (recentWorkspaces == null) {
324: recentWorkspaces = new String[RECENT_MAX_LENGTH];
325: }
326: }
327:
328: return true;
329: }
330:
331: /**
332: * Return the current (persisted) value of the "showDialog on startup"
333: * preference. Return the global default if the file cannot be accessed.
334: */
335: public static boolean getShowDialogValue() {
336: // TODO See the long comment in #readPersistedData -- when the
337: // transition time is over this method can be changed to
338: // read the preference directly.
339:
340: ChooseWorkspaceData data = new ChooseWorkspaceData(""); //$NON-NLS-1$
341:
342: // return either the value in the file or true, which is the global
343: // default
344: return data.readPersistedData() ? data.showDialog : true;
345: }
346:
347: /**
348: * Return the current (persisted) value of the "showDialog on startup"
349: * preference. Return the global default if the file cannot be accessed.
350: */
351: public static void setShowDialogValue(boolean showDialog) {
352: // TODO See the long comment in #readPersistedData -- when the
353: // transition time is over this method can be changed to
354: // read the preference directly.
355:
356: ChooseWorkspaceData data = new ChooseWorkspaceData(""); //$NON-NLS-1$
357:
358: // update the value and write the new settings
359: data.showDialog = showDialog;
360: data.writePersistedData();
361: }
362:
363: /**
364: * Look in the config area preference store for the list of recently used
365: * workspaces.
366: *
367: * NOTE: During the transition phase the file will be checked if no config
368: * preferences are found.
369: *
370: * @return true if the values were successfully retrieved and false
371: * otherwise
372: */
373: public boolean readPersistedData() {
374: IPreferenceStore store = new ScopedPreferenceStore(
375: new ConfigurationScope(),
376: IDEWorkbenchPlugin.IDE_WORKBENCH);
377:
378: // The old way was to store this information in a file, the new is to
379: // use the configuration area preference store. To help users with the
380: // transition, this code always looks for values in the preference
381: // store; they are used if found. If there aren't any related
382: // preferences, then the file method is used instead. This class always
383: // writes to the preference store, so the fall-back should be needed no
384: // more than once per-user, per-configuration.
385:
386: // This code always sets the value of the protocol to a non-zero value
387: // (currently at 2). If the value comes back as the default (0), then
388: // none of the preferences were set, revert to the file method.
389:
390: int protocol = store
391: .getInt(IDE.Preferences.RECENT_WORKSPACES_PROTOCOL);
392: if (protocol == IPreferenceStore.INT_DEFAULT_DEFAULT
393: && readPersistedData_file()) {
394: return true;
395: }
396:
397: // 2. get value for showDialog
398: showDialog = store
399: .getBoolean(IDE.Preferences.SHOW_WORKSPACE_SELECTION_DIALOG);
400:
401: // 3. use value of numRecent to create proper length array
402: int max = store.getInt(IDE.Preferences.MAX_RECENT_WORKSPACES);
403: max = Math.max(max, RECENT_MAX_LENGTH);
404:
405: // 4. load values of recent workspaces into array
406: String workspacePathPref = store
407: .getString(IDE.Preferences.RECENT_WORKSPACES);
408: recentWorkspaces = decodeStoredWorkspacePaths(protocol, max,
409: workspacePathPref);
410:
411: return true;
412: }
413:
414: /**
415: * The the list of recent workspaces must be stored as a string in the preference node.
416: */
417: private static String encodeStoredWorkspacePaths(String[] recent) {
418: StringBuffer buff = new StringBuffer();
419:
420: String path = null;
421: for (int i = 0; i < recent.length; ++i) {
422: if (recent[i] == null) {
423: break;
424: }
425:
426: // as of 3.3.1 pump this out using newlines instead of commas
427: if (path != null) {
428: buff.append("\n"); //$NON-NLS-1$
429: }
430:
431: path = recent[i];
432: buff.append(path);
433: }
434:
435: return buff.toString();
436: }
437:
438: /**
439: * The the preference for recent workspaces must be converted from the
440: * storage string into an array.
441: */
442: private static String[] decodeStoredWorkspacePaths(int protocol,
443: int max, String prefValue) {
444: String[] paths = new String[max];
445: if (prefValue == null || prefValue.length() <= 0) {
446: return paths;
447: }
448:
449: // if we're using the latest version of the protocol use the newline as a
450: // token. Otherwise use the older comma.
451: String tokens = null;
452: switch (protocol) {
453: case PERS_ENCODING_VERSION_CONFIG_PREFS_NO_COMMAS:
454: tokens = "\n"; //$NON-NLS-1$
455: break;
456: case PERS_ENCODING_VERSION_CONFIG_PREFS:
457: tokens = ","; //$NON-NLS-1$
458: break;
459: }
460: if (tokens == null) // unknown version? corrupt file? we can't log it
461: // because we dont have a workspace yet...
462: return new String[0];
463:
464: StringTokenizer tokenizer = new StringTokenizer(prefValue,
465: tokens);
466: for (int i = 0; i < paths.length && tokenizer.hasMoreTokens(); ++i) {
467: paths[i] = tokenizer.nextToken();
468: }
469:
470: return paths;
471: }
472:
473: /**
474: * Return true if the protocol used to encode the argument memento is
475: * compatible with the receiver's implementation and false otherwise.
476: */
477: private static boolean compatibleFileProtocol(IMemento memento) {
478: IMemento protocolMemento = memento.getChild(XML.PROTOCOL);
479: if (protocolMemento == null) {
480: return false;
481: }
482:
483: Integer version = protocolMemento.getInteger(XML.VERSION);
484: return version != null
485: && version.intValue() == PERS_ENCODING_VERSION;
486: }
487:
488: /**
489: * The workspace data is stored in the well known file pointed to by the result
490: * of this method.
491: * @param create If the directory and file does not exist this parameter
492: * controls whether it will be created.
493: * @return An url to the file and null if it does not exist or could not
494: * be created.
495: */
496: private static URL getPersistenceUrl(URL baseUrl, boolean create) {
497: if (baseUrl == null) {
498: return null;
499: }
500:
501: try {
502: // make sure the directory exists
503: URL url = new URL(baseUrl, PERS_FOLDER);
504: File dir = new File(url.getFile());
505: if (!dir.exists() && (!create || !dir.mkdir())) {
506: return null;
507: }
508:
509: // make sure the file exists
510: url = new URL(dir.toURL(), PERS_FILENAME);
511: File persFile = new File(url.getFile());
512: if (!persFile.exists()
513: && (!create || !persFile.createNewFile())) {
514: return null;
515: }
516:
517: return persFile.toURL();
518: } catch (IOException e) {
519: // cannot log because instance area has not been set
520: return null;
521: }
522: }
523: }
|