1 /*******************************************************************************
2  * Copyright (c) 2000, 2017 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *******************************************************************************/
14 package org.eclipse.update.internal.configurator;
15 
16 import java.io.*;
17 import java.net.*;
18 import java.util.ArrayList;
19 import java.util.Arrays;
20 import java.util.Collection;
21 import java.util.Date;
22 import java.util.HashMap;
23 import java.util.Iterator;
24 import java.util.Map;
25 import java.util.zip.*;
26 
27 import org.eclipse.core.runtime.*;
28 import org.eclipse.osgi.service.environment.*;
29 import org.eclipse.osgi.util.NLS;
30 import org.eclipse.update.configurator.*;
31 import org.eclipse.update.configurator.IPlatformConfiguration.*;
32 import org.w3c.dom.*;
33 import org.xml.sax.*;
34 
35 
36 public class SiteEntry implements IPlatformConfiguration.ISiteEntry, IConfigurationConstants{
37 	private static final String MAC_OS_MARKER = ".DS_Store"; //$NON-NLS-1$
38 
39 	private URL url; // this is the external URL for the site
40 	private URL resolvedURL; // this is the resolved URL used internally
41 	private ISitePolicy policy;
42 	private boolean updateable = true;
43 	private Map<String, IFeatureEntry> featureEntries;
44 	private ArrayList<PluginEntry> pluginEntries;
45 	private long changeStamp;
46 	private long featuresChangeStamp;
47 	private long pluginsChangeStamp;
48 	private String linkFileName;
49 	private boolean enabled = true;
50 	private Configuration config;
51 
52 	private static FeatureParser featureParser = new FeatureParser();
53 	private static PluginParser pluginParser = new PluginParser();
54 	private static boolean isMacOS = Utils.getOS().equals(Constants.OS_MACOSX);
55 
SiteEntry(URL url)56 	public SiteEntry(URL url) {
57 		this(url,null);
58 	}
59 
SiteEntry(URL url, ISitePolicy policy)60 	public SiteEntry(URL url, ISitePolicy policy) {
61 		if (url == null)
62 			try {
63 				url = new URL("platform:/base/"); //$NON-NLS-1$ try using platform-relative URL
64 			} catch (MalformedURLException e) {
65 				url = PlatformConfiguration.getInstallURL(); // ensure we come up ... use absolute file URL
66 			}
67 
68 		if (policy == null)
69 			policy = new SitePolicy(PlatformConfiguration.getDefaultPolicy(), DEFAULT_POLICY_LIST);
70 
71 		if (url.getProtocol().equals("file")) { //$NON-NLS-1$
72 			try {
73 				// TODO remove this when platform fixes local file url's
74 				this.url = new File(url.getFile()).toURL();
75 			} catch (MalformedURLException e1) {
76 				this.url = url;
77 			}
78 		} else
79 			this.url = url;
80 
81 		this.policy = policy;
82 		this.resolvedURL = this.url;
83 	}
84 
setConfig(Configuration config)85 	public void setConfig(Configuration config) {
86 		this.config = config;
87 		if (url.getProtocol().equals("platform")) { //$NON-NLS-1$
88 			try {
89 				// resolve the config location relative to the configURL
90 				if (url.getPath().startsWith("/config")) {
91 					URL configURL = config.getURL();
92 					URL config_loc = new URL(configURL, "..");
93 					resolvedURL = PlatformConfiguration.resolvePlatformURL(url, config_loc); // 19536
94 				}
95 				else
96 					resolvedURL = PlatformConfiguration.resolvePlatformURL(url, config.getInstallURL()); // 19536
97 			} catch (IOException e) {
98 				// will use the baseline URL ...
99 			}
100 		}
101 	}
102 
getConfig()103 	public Configuration getConfig() {
104 		return config;
105 	}
106 
107 	@Override
getURL()108 	public URL getURL() {
109 		return url;
110 	}
111 
112 	@Override
getSitePolicy()113 	public ISitePolicy getSitePolicy() {
114 		return policy;
115 	}
116 
117 	@Override
setSitePolicy(ISitePolicy policy)118 	public synchronized void setSitePolicy(ISitePolicy policy) {
119 		if (policy == null)
120 			throw new IllegalArgumentException();
121 		this.policy = policy;
122 	}
123 
124 	@Override
getFeatures()125 	public String[] getFeatures() {
126 		return getDetectedFeatures();
127 	}
128 
129 	@Override
getPlugins()130 	public String[] getPlugins() {
131 
132 		ISitePolicy policy = getSitePolicy();
133 
134 		if (policy.getType() == ISitePolicy.USER_INCLUDE)
135 			return policy.getList();
136 
137 		if (policy.getType() == ISitePolicy.USER_EXCLUDE) {
138 			ArrayList<String> detectedPlugins = new ArrayList<>(Arrays.asList(getDetectedPlugins()));
139 			for (String excludedPlugin : policy.getList()) {
140 				if (detectedPlugins.contains(excludedPlugin))
141 					detectedPlugins.remove(excludedPlugin);
142 			}
143 			return detectedPlugins.toArray(new String[0]);
144 		}
145 
146 		if (policy.getType() == ISitePolicy.MANAGED_ONLY) {
147 			PluginEntry[] managedPlugins = getManagedPlugins();
148 			String[] managedPluginsURLs = new String[managedPlugins.length];
149 			for (int i=0; i<managedPlugins.length; i++)
150 				managedPluginsURLs[i] = managedPlugins[i].getURL();
151 
152 			return managedPluginsURLs;
153 		}
154 
155 		// bad policy type
156 		return new String[0];
157 	}
158 
getManagedPlugins()159 	private PluginEntry[] getManagedPlugins() {
160 		// Note:
161 		// We detect all the plugins on the site, but it would be faster
162 		// to just lookup the plugins that correspond to the entries found in each feature.
163 		// TODO fix the above
164 		if (pluginEntries == null)
165 			detectPlugins();
166 		if (featureEntries == null)
167 			detectFeatures();
168 
169 		// cache all the plugin entries for faster lookup later
170 		Map<VersionedIdentifier, PluginEntry> cachedPlugins = new HashMap<>(pluginEntries.size());
171 		for (PluginEntry p : pluginEntries) {
172 			cachedPlugins.put(p.getVersionedIdentifier(), p);
173 		}
174 
175 		ArrayList<PluginEntry> managedPlugins = new ArrayList<>();
176 		for (IFeatureEntry feature : featureEntries.values()) {
177 			if (!(feature instanceof FeatureEntry))
178 				continue;
179 
180 			for (PluginEntry plugin : ((FeatureEntry)feature).getPluginEntries())
181 				if (cachedPlugins.containsKey(plugin.getVersionedIdentifier()))
182 					managedPlugins.add(cachedPlugins.get(plugin.getVersionedIdentifier()));
183 
184 		}
185 		return managedPlugins.toArray(new PluginEntry[managedPlugins.size()]);
186 	}
187 
getPluginEntries()188 	public PluginEntry[] getPluginEntries() {
189 		String[] pluginURLs = getPlugins();
190 		// hash the array, for faster lookups
191 		HashMap<String, String> map = new HashMap<>(pluginURLs.length);
192 		for (String pluginURL : pluginURLs)
193 			map.put(pluginURL, pluginURL);
194 
195 		if (pluginEntries == null)
196 				detectPlugins();
197 
198 		ArrayList<PluginEntry> plugins = new ArrayList<>(pluginURLs.length);
199 		for (int i=0; i<pluginEntries.size(); i++) {
200 			PluginEntry p = pluginEntries.get(i);
201 			if (map.containsKey(p.getURL()))
202 				plugins.add(p);
203 		}
204 		return plugins.toArray(new PluginEntry[plugins.size()]);
205 	}
206 
207 	@Override
getChangeStamp()208 	public long getChangeStamp() {
209 		if (changeStamp == 0)
210 			computeChangeStamp();
211 		return changeStamp;
212 	}
213 
214 	@Override
getFeaturesChangeStamp()215 	public long getFeaturesChangeStamp() {
216 		if (featuresChangeStamp == 0)
217 			computeFeaturesChangeStamp();
218 		return featuresChangeStamp;
219 	}
220 
221 	@Override
getPluginsChangeStamp()222 	public long getPluginsChangeStamp() {
223 		if (pluginsChangeStamp == 0)
224 			computePluginsChangeStamp();
225 		return pluginsChangeStamp;
226 	}
227 
228 	@Override
isUpdateable()229 	public boolean isUpdateable() {
230 		return updateable;
231 	}
232 
setUpdateable(boolean updateable)233 	public void setUpdateable(boolean updateable) {
234 		this.updateable = updateable;
235 	}
236 
237 	@Override
isNativelyLinked()238 	public boolean isNativelyLinked() {
239 		return isExternallyLinkedSite();
240 	}
241 
getResolvedURL()242 	public URL getResolvedURL() {
243 		return resolvedURL;
244 	}
245 
246 	/**
247 	 * Detect new features (timestamp &gt; current site timestamp)
248 	 * and validates existing features (they might have been removed)
249 	 */
detectFeatures()250 	private void detectFeatures() {
251 
252 		if (featureEntries != null)
253 			validateFeatureEntries();
254 		else
255 			featureEntries = new HashMap<>();
256 
257 		if (!PlatformConfiguration.supportsDetection(resolvedURL, config.getInstallURL()))
258 			return;
259 
260 		// locate feature entries on site
261 		File siteRoot = new File(resolvedURL.getFile().replace('/', File.separatorChar));
262 		File featuresDir = new File(siteRoot, FEATURES);
263 		if (featuresDir.exists()) {
264 			// handle the installed features under the features directory
265 			File[] dirs = featuresDir.listFiles((FileFilter) f -> {
266 				// mac os folders contain a file .DS_Store in each folder, and we need to skip it (bug 76869)
267 				if (isMacOS && f.getName().equals(MAC_OS_MARKER))
268 					return false;
269 				boolean valid = f.isDirectory() && (new File(f,FEATURE_XML).exists());
270 				if (!valid)
271 					Utils.log(NLS.bind(Messages.SiteEntry_cannotFindFeatureInDir, (new String[] { f.getAbsolutePath() })));
272 				return valid;
273 			});
274 
275 			for (File dir : dirs) {
276 				try {
277 					File featureXML = new File(dir, FEATURE_XML);
278 					if (featureXML.lastModified() <= featuresChangeStamp &&
279 						dir.lastModified() <= featuresChangeStamp)
280 						continue;
281 					URL featureURL = featureXML.toURL();
282 					FeatureEntry featureEntry = featureParser.parse(featureURL);
283 					if (featureEntry != null)
284 						addFeatureEntry(featureEntry);
285 				} catch (MalformedURLException e) {
286 					Utils.log(NLS.bind(Messages.InstalledSiteParser_UnableToCreateURLForFile, (new String[] { featuresDir.getAbsolutePath() })));
287 				}
288 			}
289 		}
290 
291 		Utils.debug(resolvedURL.toString() + " located  " + featureEntries.size() + " feature(s)"); //$NON-NLS-1$ //$NON-NLS-2$
292 	}
293 
294 	/**
295 	 * Detect new plugins (timestamp &gt; current site timestamp)
296 	 * and validates existing plugins (they might have been removed)
297 	 */
detectPlugins()298 	private void detectPlugins() {
299 		boolean compareTimeStamps = false;
300 		if (pluginEntries != null) {
301 			validatePluginEntries();
302 			compareTimeStamps = true; // only pick up newer plugins
303 		} else
304 			pluginEntries = new ArrayList<>();
305 
306 		if (!PlatformConfiguration.supportsDetection(resolvedURL, config.getInstallURL()))
307 			return;
308 
309 		// locate plugin entries on site
310 		File pluginsDir = new File(resolvedURL.getFile(), PLUGINS);
311 
312 		if (pluginsDir.exists() && pluginsDir.isDirectory()) {
313 			for (File file : pluginsDir.listFiles()) {
314 				if(file.isDirectory()){
315 					detectUnpackedPlugin(file, compareTimeStamps);
316 				}else if(file.getName().endsWith(".jar")){ //$NON-NLS-1$
317 					detectPackedPlugin(file, compareTimeStamps);
318 				}else{
319 					// not bundle file
320 				}
321 			}
322 		}
323 
324 		Utils.debug(resolvedURL.toString() + " located  " + pluginEntries.size() + " plugin(s)"); //$NON-NLS-1$ //$NON-NLS-2$
325 	}
326 
327 	/**
328 	 * @param file a plugin jar
329 	 * @param compareTimeStamps set to true when looking for plugins changed since last time they were detected
330 	 */
detectPackedPlugin(File file, boolean compareTimeStamps)331 	private void detectPackedPlugin(File file, boolean compareTimeStamps) {
332 		// plugin to run directly from jar
333 		if (compareTimeStamps && file.lastModified() <= pluginsChangeStamp) {
334 			return;
335 		}
336 		String entryName = META_MANIFEST_MF;
337 		InputStream bundleManifestIn = null;
338 		InputStream pluginManifestIn = null;
339 		String pluginURL = PLUGINS + "/" + file.getName(); //$NON-NLS-1$
340 		try (ZipFile z = new ZipFile(file)){
341 			// First, check if has valid bundle manifest
342 
343 			if (z.getEntry(entryName) != null) {
344 				bundleManifestIn = z.getInputStream(new ZipEntry(entryName));
345 				BundleManifest manifest = new BundleManifest(bundleManifestIn,
346 						pluginURL);
347 				if (manifest.exists()) {
348 					addPluginEntry(manifest.getPluginEntry());
349 					return;
350 				}
351 			}
352 			// no bundle manifest, check for plugin.xml or fragment.xml
353 			entryName = PLUGIN_XML;
354 			if (z.getEntry(entryName) == null) {
355 				entryName = FRAGMENT_XML;
356 			}
357 			if (z.getEntry(entryName) != null) {
358 				pluginManifestIn = z.getInputStream(new ZipEntry(entryName));
359 				PluginEntry entry1 = pluginParser.parse(pluginManifestIn,
360 						pluginURL);
361 				addPluginEntry(entry1);
362 			}
363 		} catch (IOException e5) {
364 			String pluginFileString2 = pluginURL + "!" + entryName; //$NON-NLS-1$
365 			Utils.log(NLS.bind(Messages.InstalledSiteParser_ErrorAccessing, (new String[] { pluginFileString2 })));
366 		} catch (SAXException e3) {
367 			String pluginFileString1 = pluginURL + "!" + entryName; //$NON-NLS-1$
368 			Utils.log(NLS.bind(Messages.InstalledSiteParser_ErrorParsingFile, (new String[] { pluginFileString1 })));
369 		} finally {
370 			if (bundleManifestIn != null) {
371 				try {
372 					bundleManifestIn.close();
373 				} catch (IOException e4) {
374 				}
375 			}
376 			if (pluginManifestIn != null) {
377 				try {
378 					pluginManifestIn.close();
379 				} catch (IOException e2) {
380 				}
381 			}
382 		}
383 	}
384 	/**
385 	 * @param file a plugin directory
386 	 * @param compareTimeStamps set to true when looking for plugins changed since last time they were detected
387 	 */
detectUnpackedPlugin(File file, boolean compareTimeStamps)388 	private void detectUnpackedPlugin(File file, boolean compareTimeStamps) {
389 		// unpacked plugin
390 		long dirTimestamp = file.lastModified();
391 		File pluginFile = new File(file, META_MANIFEST_MF);
392 		try {
393 			// First, check if has valid bundle manifest
394 			BundleManifest bundleManifest = new BundleManifest(pluginFile);
395 			if (bundleManifest.exists()) {
396 				if (compareTimeStamps
397 						&& dirTimestamp <= pluginsChangeStamp
398 						&& pluginFile.lastModified() <= pluginsChangeStamp)
399 					return;
400 				PluginEntry entry = bundleManifest.getPluginEntry();
401 				addPluginEntry(entry);
402 			} else {
403 				// no bundle manifest, check for plugin.xml or fragment.xml
404 				pluginFile = new File(file, PLUGIN_XML);
405 				if (!pluginFile.exists()) {
406 					pluginFile = new File(file, FRAGMENT_XML);
407 				}
408 				if (pluginFile.exists() && !pluginFile.isDirectory()) {
409 					// TODO in the future, assume that the timestamps are not
410 					// reliable,
411 					// or that the user manually modified an existing plugin,
412 					// so
413 					// the apparently modifed plugin may actually be configured
414 					// already.
415 					// We will need to double check for this. END to do.
416 					if (compareTimeStamps
417 							&& dirTimestamp <= pluginsChangeStamp
418 							&& pluginFile.lastModified() <= pluginsChangeStamp)
419 						return;
420 					PluginEntry entry = pluginParser.parse(pluginFile);
421 					addPluginEntry(entry);
422 				}
423 			}
424 		} catch (IOException e) {
425 			String pluginFileString = pluginFile.getAbsolutePath();
426 			if (ConfigurationActivator.DEBUG)
427 				Utils.log(Utils.newStatus(NLS.bind(Messages.InstalledSiteParser_ErrorParsingFile, (new String[] { pluginFileString })), e));
428 			else
429 				Utils.log(NLS.bind(Messages.InstalledSiteParser_ErrorAccessing, (new String[] { pluginFileString })));
430 		} catch (SAXException e) {
431 			String pluginFileString = pluginFile.getAbsolutePath();
432 			Utils.log(NLS.bind(Messages.InstalledSiteParser_ErrorParsingFile, (new String[] { pluginFileString })));
433 		}
434 	}
435 
436 	/**
437 	 * @return list of feature url's (relative to site)
438 	 */
getDetectedFeatures()439 	private synchronized String[] getDetectedFeatures() {
440 		if (featureEntries == null)
441 			detectFeatures();
442 		String[] features = new String[featureEntries.size()];
443 		Iterator<IFeatureEntry> iterator = featureEntries.values().iterator();
444 		for (int i=0; i<features.length; i++)
445 			features[i] = ((FeatureEntry)iterator.next()).getURL();
446 		return features;
447 	}
448 
449 	/**
450 	 * @return list of plugin url's (relative to site)
451 	 */
getDetectedPlugins()452 	private synchronized String[] getDetectedPlugins() {
453 		if (pluginEntries == null)
454 			detectPlugins();
455 
456 		String[] plugins = new String[pluginEntries.size()];
457 		for (int i=0; i<plugins.length; i++)
458 			plugins[i] = pluginEntries.get(i).getURL();
459 		return plugins;
460 	}
461 
computeChangeStamp()462 	private void computeChangeStamp() {
463 		changeStamp = Math.max(computeFeaturesChangeStamp(), computePluginsChangeStamp());
464 //		changeStampIsValid = true;
465 	}
466 
computeFeaturesChangeStamp()467 	private synchronized long computeFeaturesChangeStamp() {
468 		if (featuresChangeStamp > 0)
469 			return featuresChangeStamp;
470 
471 		long start = 0;
472 		if (ConfigurationActivator.DEBUG)
473 			start = (new Date()).getTime();
474 		String[] features = getFeatures();
475 
476 		// compute stamp for the features directory
477 		long dirStamp = 0;
478 		if (PlatformConfiguration.supportsDetection(resolvedURL, config.getInstallURL())) {
479 			File root = new File(resolvedURL.getFile().replace('/', File.separatorChar));
480 			File featuresDir = new File(root, FEATURES);
481 			dirStamp = featuresDir.lastModified();
482 		}
483 		featuresChangeStamp = Math.max(dirStamp, computeStamp(features));
484 		if (ConfigurationActivator.DEBUG) {
485 			long end = (new Date()).getTime();
486 			Utils.debug(resolvedURL.toString() + " feature stamp: " + featuresChangeStamp + " in " + (end - start) + "ms"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
487 		}
488 		return featuresChangeStamp;
489 	}
490 
computePluginsChangeStamp()491 	private synchronized long computePluginsChangeStamp() {
492 		if (pluginsChangeStamp > 0)
493 			return pluginsChangeStamp;
494 
495 		if (!PlatformConfiguration.supportsDetection(resolvedURL, config.getInstallURL())) {
496 			Utils.log(NLS.bind(Messages.SiteEntry_computePluginStamp, (new String[] { resolvedURL.toExternalForm() })));
497 			return 0;
498 		}
499 
500 		// compute stamp for the plugins directory
501 		File root = new File(resolvedURL.getFile().replace('/', File.separatorChar));
502 		File pluginsDir = new File(root, PLUGINS);
503 		if (!pluginsDir.exists() || !pluginsDir.isDirectory()) {
504 			Utils.debug(NLS.bind(Messages.SiteEntry_pluginsDir, (new String[] { pluginsDir.getAbsolutePath() })));
505 			return 0;
506 		}
507 
508 		pluginsChangeStamp = pluginsDir.lastModified();
509 		return pluginsChangeStamp;
510 	}
511 
computeStamp(String[] targets)512 	private long computeStamp(String[] targets) {
513 
514 		long result = 0;
515 		if (!PlatformConfiguration.supportsDetection(resolvedURL, config.getInstallURL())) {
516 			// NOTE:  this path should not be executed until we support running
517 			//        from an arbitrary URL (in particular from http server). For
518 			//        now just compute stamp across the list of names. Eventually
519 			//        when general URLs are supported we need to do better (factor
520 			//        in at least the existence of the target). However, given this
521 			//        code executes early on the startup sequence we need to be
522 			//        extremely mindful of performance issues.
523 			// In fact, we should get the last modified from the connection
524 			for (String target : targets)
525 				result ^= target.hashCode();
526 			Utils.debug("*WARNING* computing stamp using URL hashcodes only"); //$NON-NLS-1$
527 		} else {
528 			// compute stamp across local targets
529 			File rootFile = new File(resolvedURL.getFile().replace('/', File.separatorChar));
530 			if (rootFile.exists()) {
531 				File f = null;
532 				for (String target : targets) {
533 					f = new File(rootFile, target);
534 					if (f.exists())
535 						result = Math.max(result, f.lastModified());
536 				}
537 			}
538 		}
539 
540 		return result;
541 	}
542 
setLinkFileName(String linkFileName)543 	public void setLinkFileName(String linkFileName) {
544 		this.linkFileName = linkFileName;
545 	}
546 
getLinkFileName()547 	public String getLinkFileName() {
548 		return linkFileName;
549 	}
550 
isExternallyLinkedSite()551 	public boolean isExternallyLinkedSite() {
552 		return (linkFileName != null && !linkFileName.trim().isEmpty());
553 	}
554 
refresh()555 	public synchronized void refresh() {
556 		// reset computed values. Will be updated on next access.
557 		featuresChangeStamp = 0;
558 		pluginsChangeStamp = 0;
559 		changeStamp = 0;
560 		featureEntries = null;
561 		pluginEntries = null;
562 	}
563 
refreshPlugins()564 	public void refreshPlugins() {
565 		// reset computed values. Will be updated on next access.
566 		pluginsChangeStamp = 0;
567 		changeStamp = 0;
568 		pluginEntries = null;
569 	}
570 
addFeatureEntry(IFeatureEntry feature)571 	public void addFeatureEntry(IFeatureEntry feature) {
572 		if (featureEntries == null)
573 			featureEntries = new HashMap<>();
574 		// Make sure we keep the larger version of same feature
575 		IFeatureEntry existing = featureEntries.get(feature.getFeatureIdentifier());
576 		if (existing != null) {
577 			VersionedIdentifier existingVersion = new VersionedIdentifier(existing.getFeatureIdentifier(), existing.getFeatureVersion());
578 			VersionedIdentifier newVersion = new VersionedIdentifier(feature.getFeatureIdentifier(), feature.getFeatureVersion());
579 			if (existingVersion.getVersion().compareTo(newVersion.getVersion()) < 0) {
580 				featureEntries.put(feature.getFeatureIdentifier(), feature);
581 				pluginsChangeStamp = 0;
582 			} else if (existingVersion.equals(newVersion)) {
583 				// log error if same feature version/id but a different url
584 				if (feature instanceof FeatureEntry && existing instanceof FeatureEntry &&
585 						!((FeatureEntry)feature).getURL().equals(((FeatureEntry)existing).getURL()))
586 				Utils.log(NLS.bind(Messages.SiteEntry_duplicateFeature, (new String[] { getURL().toExternalForm(), existing.getFeatureIdentifier() })));
587 			}
588 		} else {
589 			featureEntries.put(feature.getFeatureIdentifier(), feature);
590 			pluginsChangeStamp = 0;
591 		}
592 		if (feature instanceof FeatureEntry)
593 			((FeatureEntry)feature).setSite(this);
594 	}
595 
getFeatureEntries()596 	public FeatureEntry[] getFeatureEntries() {
597 		if (featureEntries == null)
598 			detectFeatures();
599 
600 		if (featureEntries == null)
601 			return new FeatureEntry[0];
602 		return featureEntries.values().toArray(new FeatureEntry[featureEntries.size()]);
603 	}
604 
addPluginEntry(PluginEntry plugin)605 	public void addPluginEntry(PluginEntry plugin) {
606 		if (pluginEntries == null)
607 			pluginEntries = new ArrayList<>();
608 		// Note: we could use the latest version of the same plugin, like we do for features, but we let the runtime figure it out
609 		pluginEntries.add(plugin);
610 	}
611 
getAllPluginEntries()612 	public PluginEntry[] getAllPluginEntries() {
613 		if (pluginEntries == null)
614 			detectPlugins();
615 		return pluginEntries.toArray(new PluginEntry[pluginEntries.size()]);
616 	}
617 
loadFromDisk(long lastChange)618 	public void loadFromDisk(long lastChange) throws CoreException{
619 		featuresChangeStamp = lastChange;
620 		pluginsChangeStamp = lastChange;
621 		detectFeatures();
622 		detectPlugins();
623 	}
624 
625 	/**
626 	 * Saves state as xml content in a given parent element
627 	 * @param doc
628 	 */
toXML(Document doc)629 	public Element toXML(Document doc) {
630 
631 		Element siteElement = doc.createElement(CFG_SITE);
632 
633 		if (getURL() != null) {
634 			URL toPersist = (config == null || config.isTransient()) ? getURL() : Utils.makeRelative(Utils.getInstallURL(), getURL());
635 			siteElement.setAttribute(CFG_URL, toPersist.toString());
636 		}
637 
638 		siteElement.setAttribute(CFG_ENABLED, isEnabled() ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$
639 		siteElement.setAttribute(CFG_UPDATEABLE, isUpdateable() ? "true" : "false"); //$NON-NLS-1$ //$NON-NLS-2$
640 		if (isExternallyLinkedSite())
641 			siteElement.setAttribute(CFG_LINK_FILE, getLinkFileName().trim().replace(File.separatorChar, '/'));
642 
643 		int type = getSitePolicy().getType();
644 		String typeString = CFG_POLICY_TYPE_UNKNOWN;
645 		try {
646 			typeString = CFG_POLICY_TYPE[type];
647 		} catch (IndexOutOfBoundsException e) {
648 			// ignore bad attribute ...
649 		}
650 		siteElement.setAttribute(CFG_POLICY, typeString);
651 		String[] list = getSitePolicy().getList();
652 		if (list.length > 0) {
653 			StringBuilder sb = new StringBuilder(256);
654 			for (int i=0; i<list.length-1; i++) {
655 				sb.append(list[i]);
656 				sb.append(',');
657 			}
658 			sb.append(list[list.length-1]);
659 			siteElement.setAttribute(CFG_LIST, sb.toString());
660 		}
661 //		// note: we don't save features inside the site element.
662 
663 		// collect feature entries
664 //		configElement.setAttribute(CFG_FEATURE_ENTRY_DEFAULT, defaultFeature);
665 		for (FeatureEntry feat : getFeatureEntries()) {
666 			Element featureElement = feat.toXML(doc);
667 			siteElement.appendChild(featureElement);
668 		}
669 
670 		return siteElement;
671 	}
672 
validateFeatureEntries()673 	private void validateFeatureEntries() {
674 		File root = new File(resolvedURL.getFile().replace('/', File.separatorChar));
675 		Iterator<IFeatureEntry> iterator = featureEntries.values().iterator();
676 		Collection<String> deletedFeatures = new ArrayList<>();
677 		while(iterator.hasNext()) {
678 			FeatureEntry feature = (FeatureEntry)iterator.next();
679 			// Note: in the future, we can check for absolute url as well.
680 			//       For now, feature url is features/org.eclipse.foo/feature.xml
681 			File featureXML = new File(root, feature.getURL());
682 			if (!featureXML.exists())
683 				deletedFeatures.add(feature.getFeatureIdentifier());
684 		}
685 		for (String string : deletedFeatures) {
686 			featureEntries.remove(string);
687 		}
688 	}
689 
validatePluginEntries()690 	private void validatePluginEntries() {
691 		File root = new File(resolvedURL.getFile().replace('/', File.separatorChar));
692 		Collection<PluginEntry> deletedPlugins = new ArrayList<>();
693 		for (PluginEntry plugin : pluginEntries) {
694 			// Note: in the future, we can check for absolute url as well.
695 			//       For now, feature url is plugins/org.eclipse.foo/plugin.xml
696 			File pluginLocation = new File(root, plugin.getURL());
697 			if (!pluginLocation.exists())
698 				deletedPlugins.add(plugin);
699 		}
700 		for (PluginEntry pluginEntry : deletedPlugins) {
701 			pluginEntries.remove(pluginEntry);
702 		}
703 	}
704 
isEnabled()705 	public boolean isEnabled() {
706 		return enabled;
707 	}
708 
setEnabled(boolean enable)709 	public void setEnabled(boolean enable) {
710 		this.enabled = enable;
711 	}
712 
getFeatureEntry(String id)713 	public FeatureEntry getFeatureEntry(String id) {
714 		for (FeatureEntry feature : getFeatureEntries())
715 			if (feature.getFeatureIdentifier().equals(id))
716 				return feature;
717 		return null;
718 	}
719 
720 
unconfigureFeatureEntry(IFeatureEntry feature)721 	public boolean unconfigureFeatureEntry(IFeatureEntry feature) {
722 		FeatureEntry existingFeature = getFeatureEntry(feature.getFeatureIdentifier());
723 		if (existingFeature != null)
724 			featureEntries.remove(existingFeature.getFeatureIdentifier());
725 		return existingFeature != null;
726 	}
727 
728 	/*
729 	 * This is a bit of a hack.
730 	 * When no features were added to the site, but the site is initialized from platform.xml
731 	 * we need to set the feature set to empty, so we don't try to detect them.
732 	 */
initialized()733 	public void initialized() {
734 		if (featureEntries == null)
735 			featureEntries = new HashMap<>();
736 	}
737 }
738