Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 4cd2c43b097ecedffc5007b22ee8c7d8b011490e (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
/*******************************************************************************
 *  Copyright (c) 2008, 2017 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
 *     Code 9 - ongoing development
 *******************************************************************************/
package org.eclipse.equinox.internal.p2.extensionlocation;

import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import org.eclipse.core.runtime.*;
import org.eclipse.equinox.internal.p2.core.helpers.LogHelper;
import org.eclipse.equinox.internal.p2.publisher.eclipse.FeatureParser;
import org.eclipse.equinox.internal.p2.update.Site;
import org.eclipse.equinox.internal.provisional.p2.directorywatcher.*;
import org.eclipse.equinox.p2.core.ProvisionException;
import org.eclipse.equinox.p2.publisher.eclipse.*;
import org.eclipse.osgi.service.resolver.BundleDescription;

/**
 * @since 1.0
 */
public class SiteListener extends DirectoryChangeListener {

	public static final String SITE_POLICY = "org.eclipse.update.site.policy"; //$NON-NLS-1$
	public static final String SITE_LIST = "org.eclipse.update.site.list"; //$NON-NLS-1$
	private static final String FEATURES = "features"; //$NON-NLS-1$
	private static final String PLUGINS = "plugins"; //$NON-NLS-1$
	private static final String FEATURE_MANIFEST = "feature.xml"; //$NON-NLS-1$
	public static final Object UNINITIALIZED = "uninitialized"; //$NON-NLS-1$
	public static final Object INITIALIZING = "initializing"; //$NON-NLS-1$
	public static final Object INITIALIZED = "initialized"; //$NON-NLS-1$

	private String policy;
	private String[] list;
	private String siteLocation;
	private DirectoryChangeListener delegate;
	private String[] managedFiles;
	private String[] toBeRemoved;

	/*
	 * Return true if the given list contains the full path of the given file 
	 * handle. Return false otherwise.
	 */
	private static boolean contains(String[] plugins, File file) {
		String filename = file.getAbsolutePath();
		for (int i = 0; i < plugins.length; i++)
			if (filename.endsWith(plugins[i]))
				return true;
		return false;
	}

	/**
	 * Converts a list of file names to a normalized form suitable for comparison.
	 */
	private String[] normalize(String[] filenames) {
		for (int i = 0; i < filenames.length; i++)
			filenames[i] = new File(filenames[i]).toString();
		return filenames;
	}

	/**
	 * Given one repo and a base location, ensure cause the other repo to be loaded and then 
	 * poll the base location once updating the repositories accordingly.  This method is used to 
	 * ensure that both the metadata and artifact repos corresponding to one location are 
	 * synchronized in one go.  It is expected that both repos have been previously created
	 * so simply loading them here will work and that all their properties etc have been configured
	 * previously.
	 * @param metadataRepository
	 * @param artifactRepository
	 * @param base
	 */
	public static synchronized void synchronizeRepositories(ExtensionLocationMetadataRepository metadataRepository, ExtensionLocationArtifactRepository artifactRepository, File base) {
		try {
			if (metadataRepository == null) {
				artifactRepository.reload();
				ExtensionLocationMetadataRepositoryFactory factory = new ExtensionLocationMetadataRepositoryFactory();
				factory.setAgent(artifactRepository.getProvisioningAgent());
				metadataRepository = (ExtensionLocationMetadataRepository) factory.load(artifactRepository.getLocation(), 0, null);
			} else if (artifactRepository == null) {
				metadataRepository.reload();
				ExtensionLocationArtifactRepositoryFactory factory = new ExtensionLocationArtifactRepositoryFactory();
				factory.setAgent(metadataRepository.getProvisioningAgent());
				artifactRepository = (ExtensionLocationArtifactRepository) factory.load(metadataRepository.getLocation(), 0, null);
			}
		} catch (ProvisionException e) {
			// TODO need proper error handling here.  What should we do if there is a failure
			// when loading "the other" repo?
			e.printStackTrace();
			return;
		}

		artifactRepository.state(INITIALIZING);
		metadataRepository.state(INITIALIZING);
		File plugins = new File(base, PLUGINS);
		File features = new File(base, FEATURES);
		DirectoryWatcher watcher = new DirectoryWatcher(new File[] {plugins, features});
		//  here we have to sync with the inner repos as the extension location repos are 
		// read-only wrappers.
		DirectoryChangeListener listener = new RepositoryListener(metadataRepository.metadataRepository, artifactRepository.artifactRepository);
		if (metadataRepository.getProperties().get(SiteListener.SITE_POLICY) != null)
			listener = new SiteListener(metadataRepository.getProperties(), metadataRepository.getLocation().toString(), new BundlePoolFilteredListener(listener));
		watcher.addListener(listener);
		watcher.poll();
		artifactRepository.state(INITIALIZED);
		metadataRepository.state(INITIALIZED);
	}

	/*
	 * Create a new site listener on the given site.
	 */
	public SiteListener(Map<String, String> properties, String url, DirectoryChangeListener delegate) {
		this.siteLocation = url;
		this.delegate = delegate;
		this.policy = properties.get(SITE_POLICY);
		Collection<String> listCollection = new HashSet<>();
		String listString = properties.get(SITE_LIST);
		if (listString != null)
			for (StringTokenizer tokenizer = new StringTokenizer(listString, ","); tokenizer.hasMoreTokens();) //$NON-NLS-1$
				listCollection.add(tokenizer.nextToken());
		this.list = normalize(listCollection.toArray(new String[listCollection.size()]));
	}

	@Override
	public boolean isInterested(File file) {
		// make sure that our delegate and super-class are both interested in 
		// the file before we consider it
		if (!delegate.isInterested(file))
			return false;
		if (Site.POLICY_MANAGED_ONLY.equals(policy)) {
			// we only want plug-ins referenced by features
			return contains(getManagedFiles(), file);
		} else if (Site.POLICY_USER_EXCLUDE.equals(policy)) {
			// ensure the file doesn't refer to a plug-in in our list
			if (contains(list, file))
				return false;
		} else if (Site.POLICY_USER_INCLUDE.equals(policy)) {
			// we are only interested in plug-ins in the list
			if (!contains(list, file))
				return false;
		} else {
			// shouldn't happen... unknown policy type
			return false;
		}
		// at this point we have either a user-include or user-exclude policy set
		// and we think we are interested in the file. we should first check to
		// see if it is in the list of things to be removed
		return !isToBeRemoved(file);
	}

	/*
	 * Return a boolean value indicating whether or not the feature pointed to
	 * by the given file is in the update manager's list of features to be
	 * uninstalled in its clean-up phase.
	 */
	private boolean isToBeRemoved(File file) {
		String[] removed = getToBeRemoved();
		if (removed.length == 0)
			return false;
		Feature feature = getFeature(file);
		if (feature == null)
			return false;
		for (int i = 0; i < removed.length; i++) {
			String line = removed[i];
			// the line is a versioned identifier which is id_version
			if (line.equals(feature.getId() + '_' + feature.getVersion()))
				return true;
		}
		return false;
	}

	/*
	 * Parse and return the feature.xml file in the given location. 
	 * Can return null.
	 */
	private Feature getFeature(File location) {
		if (location.isFile())
			return null;
		File manifest = new File(location, FEATURE_MANIFEST);
		if (!manifest.exists())
			return null;
		FeatureParser parser = new FeatureParser();
		return parser.parse(location);
	}

	/*
	 * Return an array describing the list of features are are going
	 * to be removed by the update manager in its clean-up phase.
	 * The strings are in the format of versioned identifiers: id_version
	 */
	private String[] getToBeRemoved() {
		if (toBeRemoved != null)
			return toBeRemoved;
		File configurationLocation = Activator.getConfigurationLocation();
		if (configurationLocation == null) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Unable to compute the configuration location.")); //$NON-NLS-1$
			toBeRemoved = new String[0];
			return toBeRemoved;
		}
		File toBeUninstalledFile = new File(configurationLocation, "org.eclipse.update/toBeUninstalled"); //$NON-NLS-1$
		if (!toBeUninstalledFile.exists()) {
			toBeRemoved = new String[0];
			return toBeRemoved;
		}
		// set it to be empty here in case we don't have a match in the file
		toBeRemoved = new String[0];
		Properties properties = new Properties();
		try (InputStream input = new BufferedInputStream(new FileInputStream(toBeUninstalledFile))) {
			properties.load(input);
		} catch (IOException e) {
			// TODO
		}
		String urlString = siteLocation;
		if (urlString.endsWith(Constants.EXTENSION_LOCATION))
			urlString = urlString.substring(0, urlString.length() - Constants.EXTENSION_LOCATION.length());
		List<String> result = new ArrayList<>();
		for (Enumeration<Object> e = properties.elements(); e.hasMoreElements();) {
			String line = (String) e.nextElement();
			StringTokenizer tokenizer = new StringTokenizer(line, ";"); //$NON-NLS-1$
			String targetSite = tokenizer.nextToken();
			try {
				// the urlString is coming from the site location which is an encoded URI
				// so we need to encode the targetSite string before we check for equality
				if (!urlString.equals(URIUtil.fromString(targetSite).toString()))
					continue;
			} catch (URISyntaxException e1) {
				// shouldn't happen
				e1.printStackTrace();
				continue;
			}
			result.add(tokenizer.nextToken());
		}
		toBeRemoved = result.toArray(new String[result.size()]);
		return toBeRemoved;
	}

	/*
	 * Return an array of files which are managed. This includes all of the features
	 * for this site, as well as the locations for all the plug-ins referenced by those
	 * features.
	 */
	private String[] getManagedFiles() {
		if (managedFiles != null)
			return managedFiles;
		List<String> result = new ArrayList<>();
		File siteFile;
		try {
			siteFile = URIUtil.toFile(new URI(siteLocation));
		} catch (URISyntaxException e) {
			LogHelper.log(new Status(IStatus.ERROR, Activator.ID, "Unable to create a URL from site location: " + siteLocation, e)); //$NON-NLS-1$
			return new String[0];
		}
		Map<String, File> pluginCache = getPlugins(siteFile);
		Map<File, Feature> featureCache = getFeatures(siteFile);
		for (File featureFile : featureCache.keySet()) {
			// add the feature path
			result.add(featureFile.toString());
			Feature feature = featureCache.get(featureFile);
			FeatureEntry[] entries = feature.getEntries();
			for (int inner = 0; inner < entries.length; inner++) {
				FeatureEntry entry = entries[inner];
				// grab the right location from the plug-in cache
				String key = entry.getId() + '/' + entry.getVersion();
				File pluginLocation = pluginCache.get(key);
				if (pluginLocation != null)
					result.add(pluginLocation.toString());
			}
		}
		managedFiles = normalize(result.toArray(new String[result.size()]));
		return managedFiles;
	}

	/*
	 * Iterate over the feature directory and return a map of 
	 * File to Feature objects (from the generator bundle)
	 */
	private Map<File, Feature> getFeatures(File location) {
		Map<File, Feature> result = new HashMap<>();
		File featureDir = new File(location, FEATURES);
		File[] children = featureDir.listFiles();
		for (int i = 0; children != null && i < children.length; i++) {
			File featureLocation = children[i];
			if (featureLocation.isDirectory() && featureLocation.getParentFile() != null && featureLocation.getParentFile().getName().equals("features") && new File(featureLocation, "feature.xml").exists()) {//$NON-NLS-1$ //$NON-NLS-2$
				FeatureParser parser = new FeatureParser();
				Feature entry = parser.parse(featureLocation);
				if (entry != null)
					result.put(featureLocation, entry);
			}
		}
		return result;
	}

	/*
	 * Iterate over the plugins directory and return a map of
	 * plug-in id/version to File locations.
	 */
	private Map<String, File> getPlugins(File location) {
		File[] plugins = new File(location, PLUGINS).listFiles();
		Map<String, File> result = new HashMap<>();
		for (int i = 0; plugins != null && i < plugins.length; i++) {
			File bundleLocation = plugins[i];
			if (bundleLocation.isDirectory() || bundleLocation.getName().endsWith(".jar")) { //$NON-NLS-1$
				BundleDescription description = BundlesAction.createBundleDescriptionIgnoringExceptions(bundleLocation);
				if (description != null) {
					String id = description.getSymbolicName();
					String version = description.getVersion().toString();
					result.put(id + '/' + version, bundleLocation);
				}
			}
		}
		return result;
	}

	@Override
	public boolean added(File file) {
		return delegate.added(file);
	}

	@Override
	public boolean changed(File file) {
		return delegate.changed(file);
	}

	@Override
	public Long getSeenFile(File file) {
		return delegate.getSeenFile(file);
	}

	@Override
	public boolean removed(File file) {
		return delegate.removed(file);
	}

	@Override
	public void startPoll() {
		delegate.startPoll();
	}

	@Override
	public void stopPoll() {
		delegate.stopPoll();
	}
}

Back to the top