View Javadoc
1   /*
2    * Copyright (C) 2007-2012 Argeo GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.argeo.osgi.boot;
17  
18  import java.io.BufferedReader;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.InputStreamReader;
22  import java.net.URI;
23  import java.net.URISyntaxException;
24  import java.net.URL;
25  import java.nio.file.DirectoryStream;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.nio.file.PathMatcher;
29  import java.nio.file.Paths;
30  import java.util.ArrayList;
31  import java.util.List;
32  import java.util.SortedMap;
33  import java.util.StringTokenizer;
34  import java.util.TreeMap;
35  import java.util.jar.JarEntry;
36  import java.util.jar.JarInputStream;
37  import java.util.jar.Manifest;
38  
39  import org.osgi.framework.Constants;
40  import org.osgi.framework.Version;
41  
42  /**
43   * A distribution bundle is a bundle within a maven-like distribution
44   * groupId:Bundle-SymbolicName:Bundle-Version which references others OSGi
45   * bundle. It is not required to be OSGi complete also it will generally be
46   * expected that it is. The root of the repository is computed based on the file
47   * name of the URL and of the content of the index.
48   */
49  public class DistributionBundle {
50  	private final static String INDEX_FILE_NAME = "modularDistribution.csv";
51  
52  	private final String url;
53  
54  	private Manifest manifest;
55  	private String symbolicName;
56  	private String version;
57  
58  	/** can be null */
59  	private String baseUrl;
60  	/** can be null */
61  	private String relativeUrl;
62  	private String localCache;
63  
64  	private List<OsgiArtifact> artifacts;
65  
66  	private String separator = ",";
67  
68  	public DistributionBundle(String url) {
69  		this.url = url;
70  	}
71  
72  	public DistributionBundle(String baseUrl, String relativeUrl, String localCache) {
73  		if (baseUrl == null || !baseUrl.endsWith("/"))
74  			throw new OsgiBootException("Base url " + baseUrl + " badly formatted");
75  		if (relativeUrl.startsWith("http") || relativeUrl.startsWith("file:"))
76  			throw new OsgiBootException("Relative URL " + relativeUrl + " badly formatted");
77  		this.url = constructUrl(baseUrl, relativeUrl);
78  		this.baseUrl = baseUrl;
79  		this.relativeUrl = relativeUrl;
80  		this.localCache = localCache;
81  	}
82  
83  	protected String constructUrl(String baseUrl, String relativeUrl) {
84  		try {
85  			if (relativeUrl.indexOf('*') >= 0) {
86  				if (!baseUrl.startsWith("file:"))
87  					throw new IllegalArgumentException(
88  							"Wildcard support only for file:, badly formatted " + baseUrl + " and " + relativeUrl);
89  				Path basePath = Paths.get(new URI(baseUrl));
90  				// Path basePath = Paths.get(new URI(baseUrl));
91  				// int li = relativeUrl.lastIndexOf('/');
92  				// String relativeDir = relativeUrl.substring(0, li);
93  				// String relativeFile = relativeUrl.substring(li,
94  				// relativeUrl.length());
95  				String pattern = "glob:" + basePath + '/' + relativeUrl;
96  				PathMatcher pm = basePath.getFileSystem().getPathMatcher(pattern);
97  				SortedMap<Version, Path> res = new TreeMap<>();
98  				checkDir(basePath, pm, res);
99  				if (res.size() == 0)
100 					throw new OsgiBootException("No file matching " + relativeUrl + " found in " + baseUrl);
101 				return res.get(res.firstKey()).toUri().toString();
102 			} else {
103 				return baseUrl + relativeUrl;
104 			}
105 		} catch (Exception e) {
106 			throw new OsgiBootException("Cannot build URL from " + baseUrl + " and " + relativeUrl, e);
107 		}
108 	}
109 
110 	private void checkDir(Path dir, PathMatcher pm, SortedMap<Version, Path> res) throws IOException {
111 		try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
112 			for (Path path : ds) {
113 				if (Files.isDirectory(path))
114 					checkDir(path, pm, res);
115 				else if (pm.matches(path)) {
116 					String fileName = path.getFileName().toString();
117 					fileName = fileName.substring(0, fileName.lastIndexOf('.'));
118 					if (fileName.endsWith("-SNAPSHOT"))
119 						fileName = fileName.substring(0, fileName.lastIndexOf('-')) + ".SNAPSHOT";
120 					fileName = fileName.substring(fileName.lastIndexOf('-') + 1);
121 					Version version = new Version(fileName);
122 					res.put(version, path);
123 				}
124 			}
125 		}
126 	}
127 
128 	public void processUrl() {
129 		JarInputStream jarIn = null;
130 		try {
131 			URL u = new URL(url);
132 
133 			// local cache
134 			URI localUri = new URI(localCache + relativeUrl);
135 			Path localPath = Paths.get(localUri);
136 			if (Files.exists(localPath))
137 				u = localUri.toURL();
138 			jarIn = new JarInputStream(u.openStream());
139 
140 			// meta data
141 			manifest = jarIn.getManifest();
142 			symbolicName = manifest.getMainAttributes().getValue(Constants.BUNDLE_SYMBOLICNAME);
143 			version = manifest.getMainAttributes().getValue(Constants.BUNDLE_VERSION);
144 
145 			JarEntry indexEntry;
146 			while ((indexEntry = jarIn.getNextJarEntry()) != null) {
147 				String entryName = indexEntry.getName();
148 				if (entryName.equals(INDEX_FILE_NAME)) {
149 					break;
150 				}
151 				jarIn.closeEntry();
152 			}
153 
154 			// list artifacts
155 			if (indexEntry == null)
156 				throw new OsgiBootException("No index " + INDEX_FILE_NAME + " in " + url);
157 			artifacts = listArtifacts(jarIn);
158 			jarIn.closeEntry();
159 
160 			// find base URL
161 			// won't work if distribution artifact is not listed
162 			for (int i = 0; i < artifacts.size(); i++) {
163 				OsgiArtifact osgiArtifact = (OsgiArtifact) artifacts.get(i);
164 				if (osgiArtifact.getSymbolicName().equals(symbolicName) && osgiArtifact.getVersion().equals(version)) {
165 					String relativeUrl = osgiArtifact.getRelativeUrl();
166 					if (url.endsWith(relativeUrl)) {
167 						baseUrl = url.substring(0, url.length() - osgiArtifact.getRelativeUrl().length());
168 						break;
169 					}
170 				}
171 			}
172 		} catch (Exception e) {
173 			throw new OsgiBootException("Cannot list URLs from " + url, e);
174 		} finally {
175 			if (jarIn != null)
176 				try {
177 					jarIn.close();
178 				} catch (IOException e) {
179 					// silent
180 				}
181 		}
182 	}
183 
184 	protected List<OsgiArtifact> listArtifacts(InputStream in) {
185 		List<OsgiArtifact> osgiArtifacts = new ArrayList<OsgiArtifact>();
186 		BufferedReader reader = null;
187 		try {
188 			reader = new BufferedReader(new InputStreamReader(in));
189 			String line = null;
190 			lines: while ((line = reader.readLine()) != null) {
191 				StringTokenizer st = new StringTokenizer(line, separator);
192 				String moduleName = st.nextToken();
193 				String moduleVersion = st.nextToken();
194 				String relativeUrl = st.nextToken();
195 				if (relativeUrl.endsWith(".pom"))
196 					continue lines;
197 				osgiArtifacts.add(new OsgiArtifact(moduleName, moduleVersion, relativeUrl));
198 			}
199 		} catch (Exception e) {
200 			throw new OsgiBootException("Cannot list artifacts", e);
201 		}
202 		return osgiArtifacts;
203 	}
204 
205 	/** Convenience method */
206 	public static DistributionBundle processUrl(String baseUrl, String relativeUrl, String localCache) {
207 		DistributionBundle distributionBundle = new DistributionBundle(baseUrl, relativeUrl, localCache);
208 		distributionBundle.processUrl();
209 		return distributionBundle;
210 	}
211 
212 	/**
213 	 * List full URLs of the bundles, based on base URL, usable directly for
214 	 * download.
215 	 */
216 	public List<String> listUrls() {
217 		if (baseUrl == null)
218 			throw new OsgiBootException("Base URL is not set");
219 
220 		if (artifacts == null)
221 			throw new OsgiBootException("Artifact list not initialized");
222 
223 		List<String> urls = new ArrayList<String>();
224 		for (int i = 0; i < artifacts.size(); i++) {
225 			OsgiArtifact osgiArtifact = (OsgiArtifact) artifacts.get(i);
226 			// local cache
227 			URI localUri;
228 			try {
229 				localUri = new URI(localCache + relativeUrl);
230 			} catch (URISyntaxException e) {
231 				OsgiBootUtils.warn(e.getMessage());
232 				localUri = null;
233 			}
234 			Version version = new Version(osgiArtifact.getVersion());
235 			if (localUri != null && Files.exists(Paths.get(localUri)) && version.getQualifier() != null
236 					&& version.getQualifier().startsWith("SNAPSHOT")) {
237 				urls.add(localCache + osgiArtifact.getRelativeUrl());
238 			} else {
239 				urls.add(baseUrl + osgiArtifact.getRelativeUrl());
240 			}
241 		}
242 		return urls;
243 	}
244 
245 	public void setBaseUrl(String baseUrl) {
246 		this.baseUrl = baseUrl;
247 	}
248 
249 	/** Separator used to parse the tabular file */
250 	public void setSeparator(String modulesUrlSeparator) {
251 		this.separator = modulesUrlSeparator;
252 	}
253 
254 	public String getRelativeUrl() {
255 		return relativeUrl;
256 	}
257 
258 	/** One of the listed artifact */
259 	protected static class OsgiArtifact {
260 		private final String symbolicName;
261 		private final String version;
262 		private final String relativeUrl;
263 
264 		public OsgiArtifact(String symbolicName, String version, String relativeUrl) {
265 			super();
266 			this.symbolicName = symbolicName;
267 			this.version = version;
268 			this.relativeUrl = relativeUrl;
269 		}
270 
271 		public String getSymbolicName() {
272 			return symbolicName;
273 		}
274 
275 		public String getVersion() {
276 			return version;
277 		}
278 
279 		public String getRelativeUrl() {
280 			return relativeUrl;
281 		}
282 
283 	}
284 }