blob: 5422066461957e654addab8d8526074e34a4482b [file] [log] [blame]
Thomas Vachuska02aeb032015-01-06 22:36:30 -08001/*
Brian O'Connora09fe5b2017-08-03 21:12:30 -07002 * Copyright 2015-present Open Networking Foundation
Thomas Vachuska02aeb032015-01-06 22:36:30 -08003 *
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 */
16package org.onosproject.common.app;
17
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080018import com.google.common.collect.ImmutableList;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080019import com.google.common.collect.ImmutableSet;
Thomas Vachuska761f0042015-11-11 19:10:17 -080020import com.google.common.collect.Lists;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080021import com.google.common.io.ByteStreams;
22import com.google.common.io.Files;
23import org.apache.commons.configuration.ConfigurationException;
Changhoon Yoonb856b812015-08-10 03:47:19 +090024import org.apache.commons.configuration.HierarchicalConfiguration;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080025import org.apache.commons.configuration.XMLConfiguration;
Jian Lic35415d2016-01-14 17:22:31 -080026import org.apache.commons.lang.StringUtils;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080027import org.onlab.util.Tools;
Ray Milkey74c98a32018-07-26 10:05:49 -070028import org.onlab.util.FilePathValidator;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080029import org.onosproject.app.ApplicationDescription;
30import org.onosproject.app.ApplicationEvent;
31import org.onosproject.app.ApplicationException;
32import org.onosproject.app.ApplicationStoreDelegate;
33import org.onosproject.app.DefaultApplicationDescription;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090034import org.onosproject.core.ApplicationRole;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080035import org.onosproject.core.Version;
Changhoon Yoonb856b812015-08-10 03:47:19 +090036import org.onosproject.security.AppPermission;
37import org.onosproject.security.Permission;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080038import org.onosproject.store.AbstractStore;
39import org.slf4j.Logger;
40import org.slf4j.LoggerFactory;
41
42import java.io.ByteArrayInputStream;
43import java.io.File;
44import java.io.FileInputStream;
45import java.io.FileNotFoundException;
46import java.io.IOException;
47import java.io.InputStream;
48import java.net.URI;
HIGUCHI Yuta436f8d52015-12-07 21:17:48 -080049import java.nio.charset.StandardCharsets;
Thomas Vachuska90b453f2015-01-30 18:57:14 -080050import java.nio.file.NoSuchFileException;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080051import java.util.List;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090052import java.util.Locale;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080053import java.util.Set;
54import java.util.zip.ZipEntry;
55import java.util.zip.ZipInputStream;
56
Thomas Vachuskae18a3302015-06-23 12:48:28 -070057import static com.google.common.base.Preconditions.checkState;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080058import static com.google.common.io.ByteStreams.toByteArray;
59import static com.google.common.io.Files.createParentDirs;
60import static com.google.common.io.Files.write;
61
62/**
63 * Facility for reading application archive stream and managing application
64 * directory structure.
65 */
66public class ApplicationArchive
67 extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
68
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070069 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
70
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080071 // Magic strings to search for at the beginning of the archive stream
72 private static final String XML_MAGIC = "<?xml ";
Brian O'Connore4a4f992016-04-06 22:50:31 -070073 private static final String ZIP_MAGIC = "PK";
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080074
75 // Magic strings to search for and how deep to search it into the archive stream
76 private static final String APP_MAGIC = "<app ";
77 private static final int APP_MAGIC_DEPTH = 1024;
78
Thomas Vachuska02aeb032015-01-06 22:36:30 -080079 private static final String NAME = "[@name]";
80 private static final String ORIGIN = "[@origin]";
81 private static final String VERSION = "[@version]";
82 private static final String FEATURES_REPO = "[@featuresRepo]";
83 private static final String FEATURES = "[@features]";
Thomas Vachuska761f0042015-11-11 19:10:17 -080084 private static final String APPS = "[@apps]";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080085 private static final String DESCRIPTION = "description";
86
Jian Li01b0f5952016-01-20 11:02:07 -080087 private static final String UTILITY = "utility";
88
Jian Lic35415d2016-01-14 17:22:31 -080089 private static final String CATEGORY = "[@category]";
90 private static final String URL = "[@url]";
Simon Huntafae2f72016-03-04 21:18:23 -080091 private static final String TITLE = "[@title]";
Jian Lic35415d2016-01-14 17:22:31 -080092
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090093 private static final String ROLE = "security.role";
Changhoon Yoonb856b812015-08-10 03:47:19 +090094 private static final String APP_PERMISSIONS = "security.permissions.app-perm";
95 private static final String NET_PERMISSIONS = "security.permissions.net-perm";
96 private static final String JAVA_PERMISSIONS = "security.permissions.java-perm";
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090097
Thomas Vachuska6cc74882017-01-10 16:01:02 -080098 private static final String JAR = ".jar";
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070099 private static final String OAR = ".oar";
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800100 private static final String APP_XML = "app.xml";
Jian Li97d6b2d2016-01-20 10:13:43 -0800101 private static final String APP_PNG = "app.png";
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800102 private static final String M2_PREFIX = "m2";
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800103 private static final String FEATURES_XML = "features.xml";
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800104
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700105 private static final String ROOT = "../";
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800106 private static final String M2_ROOT = "system/";
Thomas Vachuskad5d9bcb2015-03-18 17:46:20 -0700107 private static final String APPS_ROOT = "apps/";
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800108
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700109 private File root = new File(ROOT);
110 private File appsDir = new File(root, APPS_ROOT);
111 private File m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800112
113 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700114 * Sets the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800115 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700116 * @param root top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800117 */
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700118 protected void setRootPath(String root) {
119 this.root = new File(root);
120 this.appsDir = new File(this.root, APPS_ROOT);
121 this.m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800122 }
123
124 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700125 * Returns the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800126 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700127 * @return top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800128 */
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700129 public String getRootPath() {
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700130 return root.getPath();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800131 }
132
133 /**
134 * Returns the set of installed application names.
135 *
136 * @return installed application names
137 */
138 public Set<String> getApplicationNames() {
139 ImmutableSet.Builder<String> names = ImmutableSet.builder();
140 File[] files = appsDir.listFiles(File::isDirectory);
141 if (files != null) {
142 for (File file : files) {
143 names.add(file.getName());
144 }
145 }
146 return names.build();
147 }
148
149 /**
Thomas Vachuskacf960112015-03-06 22:36:51 -0800150 * Returns the timestamp in millis since start of epoch, of when the
151 * specified application was last modified or changed state.
152 *
153 * @param appName application name
154 * @return number of millis since start of epoch
155 */
156 public long getUpdateTime(String appName) {
157 return appFile(appName, APP_XML).lastModified();
158 }
159
160 /**
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800161 * Loads the application descriptor from the specified application archive
162 * stream and saves the stream in the appropriate application archive
163 * directory.
164 *
165 * @param appName application name
166 * @return application descriptor
167 * @throws org.onosproject.app.ApplicationException if unable to read application description
168 */
169 public ApplicationDescription getApplicationDescription(String appName) {
170 try {
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700171 XMLConfiguration cfg = new XMLConfiguration();
172 cfg.setAttributeSplittingDisabled(true);
173 cfg.setDelimiterParsingDisabled(true);
174 cfg.load(appFile(appName, APP_XML));
175 return loadAppDescription(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800176 } catch (Exception e) {
177 throw new ApplicationException("Unable to get app description", e);
178 }
179 }
180
181 /**
182 * Loads the application descriptor from the specified application archive
183 * stream and saves the stream in the appropriate application archive
184 * directory.
185 *
186 * @param stream application archive stream
187 * @return application descriptor
188 * @throws org.onosproject.app.ApplicationException if unable to read the
189 * archive stream or store
190 * the application archive
191 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800192 public synchronized ApplicationDescription saveApplication(InputStream stream) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800193 try (InputStream ais = stream) {
194 byte[] cache = toByteArray(ais);
195 InputStream bis = new ByteArrayInputStream(cache);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800196
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800197 boolean plainXml = isPlainXml(cache);
198 ApplicationDescription desc = plainXml ?
199 parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700200 checkState(!appFile(desc.name(), APP_XML).exists(),
Jian Li97d6b2d2016-01-20 10:13:43 -0800201 "Application %s already installed", desc.name());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800202
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800203 if (plainXml) {
204 expandPlainApplication(cache, desc);
205 } else {
206 bis.reset();
Thomas Vachuska08b4dec2017-08-31 15:20:17 -0700207 boolean isSelfContainedJar = expandZippedApplication(bis, desc);
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800208
209 if (isSelfContainedJar) {
210 bis.reset();
211 stageSelfContainedJar(bis, desc);
212 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800213
David K. Bainbridge2133a742019-01-04 23:33:12 -0800214 /*
215 * Reset the ZIP file and reparse the app description now
216 * that the ZIP is expanded onto the filesystem. This way any
217 * file referenced as part of the description (i.e. app.png)
218 * can be loaded into the app description.
219 */
220 bis.reset();
221 desc = parseZippedAppDescription(bis);
222
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800223 bis.reset();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800224 saveApplication(bis, desc, isSelfContainedJar);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800225 }
226
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800227 installArtifacts(desc);
228 return desc;
229 } catch (IOException e) {
230 throw new ApplicationException("Unable to save application", e);
231 }
232 }
233
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800234 // Indicates whether the stream encoded in the given bytes is plain XML.
235 private boolean isPlainXml(byte[] bytes) {
Brian O'Connore4a4f992016-04-06 22:50:31 -0700236 return !substring(bytes, ZIP_MAGIC.length()).equals(ZIP_MAGIC) &&
237 (substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
238 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC));
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800239 }
240
241 // Returns the substring of maximum possible length from the specified bytes.
242 private String substring(byte[] bytes, int length) {
HIGUCHI Yuta436f8d52015-12-07 21:17:48 -0800243 return new String(bytes, 0, Math.min(bytes.length, length), StandardCharsets.UTF_8);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800244 }
245
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800246 /**
247 * Purges the application archive directory.
248 *
249 * @param appName application name
250 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800251 public synchronized void purgeApplication(String appName) {
Ray Milkey74c98a32018-07-26 10:05:49 -0700252 File appDir = new File(appsDir, appName);
253 if (!FilePathValidator.validateFile(appDir, appsDir)) {
254 throw new ApplicationException("Application attempting to create files outside the apps directory");
255 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800256 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800257 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800258 } catch (IOException e) {
259 throw new ApplicationException("Unable to purge application " + appName, e);
260 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800261 if (appDir.exists()) {
262 throw new ApplicationException("Unable to purge application " + appName);
263 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800264 }
265
266 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800267 * Returns application archive stream for the specified application. This
Thomas Vachuska08b4dec2017-08-31 15:20:17 -0700268 * will be either the application OAR file, JAR file or the plain XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800269 *
270 * @param appName application name
271 * @return application archive stream
272 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800273 public synchronized InputStream getApplicationInputStream(String appName) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800274 try {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700275 File appFile = appFile(appName, appName + OAR);
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700276 if (!appFile.exists()) {
277 appFile = appFile(appName, appName + JAR);
278 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800279 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800280 } catch (FileNotFoundException e) {
281 throw new ApplicationException("Application " + appName + " not found");
282 }
283 }
284
285 // Scans the specified ZIP stream for app.xml entry and parses it producing
286 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800287 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800288 throws IOException {
289 try (ZipInputStream zis = new ZipInputStream(stream)) {
290 ZipEntry entry;
291 while ((entry = zis.getNextEntry()) != null) {
292 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800293 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800294 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800295 }
296 zis.closeEntry();
297 }
298 }
299 throw new IOException("Unable to locate " + APP_XML);
300 }
301
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800302 // Scans the specified XML stream and parses it producing an application descriptor.
303 private ApplicationDescription parsePlainAppDescription(InputStream stream)
304 throws IOException {
305 XMLConfiguration cfg = new XMLConfiguration();
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700306 cfg.setAttributeSplittingDisabled(true);
307 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800308 try {
309 cfg.load(stream);
310 return loadAppDescription(cfg);
311 } catch (ConfigurationException e) {
312 throw new IOException("Unable to parse " + APP_XML, e);
313 }
314 }
315
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800316 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800317 String name = cfg.getString(NAME);
318 Version version = Version.version(cfg.getString(VERSION));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800319 String origin = cfg.getString(ORIGIN);
Simon Huntafae2f72016-03-04 21:18:23 -0800320
321 String title = cfg.getString(TITLE);
322 // FIXME: title should be set as attribute to APP, but fallback for now...
323 title = title == null ? name : title;
324
Jian Li01b0f5952016-01-20 11:02:07 -0800325 String category = cfg.getString(CATEGORY, UTILITY);
Jian Lic35415d2016-01-14 17:22:31 -0800326 String url = cfg.getString(URL);
327 byte[] icon = getApplicationIcon(name);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900328 ApplicationRole role = getRole(cfg.getString(ROLE));
329 Set<Permission> perms = getPermissions(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800330 String featRepo = cfg.getString(FEATURES_REPO);
331 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700332 List<String> features = ImmutableList.copyOf(cfg.getString(FEATURES).split(","));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800333
Thomas Vachuska761f0042015-11-11 19:10:17 -0800334 String apps = cfg.getString(APPS, "");
335 List<String> requiredApps = apps.isEmpty() ?
336 ImmutableList.of() : ImmutableList.copyOf(apps.split(","));
337
Jian Li8bcb4f22016-01-20 10:36:18 -0800338 // put full description to readme field
339 String readme = cfg.getString(DESCRIPTION);
Jian Lic35415d2016-01-14 17:22:31 -0800340
Jian Li8bcb4f22016-01-20 10:36:18 -0800341 // put short description to description field
342 String desc = compactDescription(readme);
Jian Lic35415d2016-01-14 17:22:31 -0800343
Ray Milkey47c95412017-09-15 10:40:48 -0700344 return DefaultApplicationDescription.builder()
345 .withName(name)
346 .withVersion(version)
347 .withTitle(title)
348 .withDescription(desc)
349 .withOrigin(origin)
350 .withCategory(category)
351 .withUrl(url)
352 .withReadme(readme)
353 .withIcon(icon)
354 .withRole(role)
355 .withPermissions(perms)
356 .withFeaturesRepo(featuresRepo)
357 .withFeatures(features)
358 .withRequiredApps(requiredApps)
359 .build();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800360 }
361
362 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800363 // Returns true of the application is a self-contained jar rather than an oar file.
364 private boolean expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800365 throws IOException {
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800366 boolean isSelfContained = false;
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800367 ZipInputStream zis = new ZipInputStream(stream);
368 ZipEntry entry;
Ray Milkey74c98a32018-07-26 10:05:49 -0700369 File appDir = new File(appsDir, desc.name());
370 if (!FilePathValidator.validateFile(appDir, appsDir)) {
371 throw new ApplicationException("Application attempting to create files outside the apps directory");
372 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800373 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800374 if (!entry.isDirectory()) {
375 byte[] data = ByteStreams.toByteArray(zis);
376 zis.closeEntry();
Ray Milkey74c98a32018-07-26 10:05:49 -0700377 if (FilePathValidator.validateZipEntry(entry, appDir)) {
Ray Milkey351d4562018-07-25 12:31:48 -0700378 File file = new File(appDir, entry.getName());
379 if (isTopLevel(file)) {
380 createParentDirs(file);
381 write(data, file);
382 } else {
383 isSelfContained = true;
384 }
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800385 } else {
Ray Milkey351d4562018-07-25 12:31:48 -0700386 throw new ApplicationException("Application Zip archive is attempting to leave application root");
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800387 }
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800388 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800389 }
390 zis.close();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800391 return isSelfContained;
392 }
393
394 // Returns true if the specified file is a top-level app file, i.e. app.xml,
395 // features.xml, .jar or a directory; false if anything else.
396 private boolean isTopLevel(File file) {
397 String name = file.getName();
David K. Bainbridge2133a742019-01-04 23:33:12 -0800398 return name.equals(APP_PNG)
399 || name.equals(APP_XML)
400 || name.endsWith(FEATURES_XML)
401 || name.endsWith(JAR)
402 || file.isDirectory();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800403 }
404
405 // Expands the self-contained JAR stream into the app-specific directory,
406 // using the bundle coordinates retrieved from the features.xml file.
407 private void stageSelfContainedJar(InputStream stream, ApplicationDescription desc)
408 throws IOException {
409 // First extract the bundle coordinates
410 String coords = getSelfContainedBundleCoordinates(desc);
411 if (coords == null) {
412 return;
413 }
414
415 // Split the coordinates into segments and build the file name.
416 String[] f = coords.substring(4).split("/");
417 String base = "m2/" + f[0].replace('.', '/') + "/" + f[1] + "/" + f[2] + "/" + f[1] + "-" + f[2];
418 String jarName = base + (f.length < 4 ? "" : "-" + f[3]) + ".jar";
419 String featuresName = base + "-features.xml";
420
421 // Create the file directory structure and copy the file there.
422 File jar = appFile(desc.name(), jarName);
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700423 boolean ok = jar.getParentFile().exists() || jar.getParentFile().mkdirs();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800424 if (ok) {
425 Files.write(toByteArray(stream), jar);
426 Files.copy(appFile(desc.name(), FEATURES_XML), appFile(desc.name(), featuresName));
427 if (!appFile(desc.name(), FEATURES_XML).delete()) {
428 log.warn("Unable to delete self-contained application {} features.xml", desc.name());
429 }
430 } else {
431 throw new IOException("Unable to save self-contained application " + desc.name());
432 }
433 }
434
435 // Returns the bundle coordinates from the features.xml file.
436 private String getSelfContainedBundleCoordinates(ApplicationDescription desc) {
437 try {
438 XMLConfiguration cfg = new XMLConfiguration();
439 cfg.setAttributeSplittingDisabled(true);
440 cfg.setDelimiterParsingDisabled(true);
441 cfg.load(appFile(desc.name(), FEATURES_XML));
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700442 return cfg.getString("feature.bundle")
443 .replaceFirst("wrap:", "")
444 .replaceFirst("\\$Bundle-.*$", "");
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800445 } catch (ConfigurationException e) {
446 log.warn("Self-contained application {} has no features.xml", desc.name());
447 return null;
448 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800449 }
450
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800451 // Saves the specified XML stream into app-specific directory.
452 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
453 throws IOException {
454 File file = appFile(desc.name(), APP_XML);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700455 checkState(!file.getParentFile().exists(), "Application already installed");
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800456 createParentDirs(file);
457 write(stream, file);
458 }
459
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800460 // Saves the specified ZIP stream into a file under app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800461 private void saveApplication(InputStream stream, ApplicationDescription desc,
462 boolean isSelfContainedJar)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800463 throws IOException {
Ray Milkey74c98a32018-07-26 10:05:49 -0700464 String name = desc.name() + (isSelfContainedJar ? JAR : OAR);
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800465 Files.write(toByteArray(stream), appFile(desc.name(), name));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800466 }
467
468 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800469 private void installArtifacts(ApplicationDescription desc) throws IOException {
470 try {
471 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
472 } catch (NoSuchFileException e) {
473 log.debug("Application {} has no M2 artifacts", desc.name());
474 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800475 }
476
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800477 /**
478 * Marks the app as active by creating token file in the app directory.
479 *
480 * @param appName application name
481 * @return true if file was created
482 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800483 protected boolean setActive(String appName) {
484 try {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800485 File active = appFile(appName, "active");
486 createParentDirs(active);
487 return active.createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800488 } catch (IOException e) {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800489 log.warn("Unable to mark app {} as active", appName, e);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800490 throw new ApplicationException("Unable to mark app as active", e);
491 }
492 }
493
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800494 /**
495 * Clears the app as active by deleting token file in the app directory.
496 *
497 * @param appName application name
498 * @return true if file was deleted
499 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800500 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800501 return appFile(appName, "active").delete() && updateTime(appName);
502 }
503
504 /**
505 * Updates the time-stamp of the app descriptor file.
506 *
507 * @param appName application name
508 * @return true if the app descriptor was updated
509 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700510 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800511 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800512 }
513
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800514 /**
515 * Indicates whether the app was marked as active by checking for token file.
516 *
517 * @param appName application name
518 * @return true if the app is marked as active
519 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800520 protected boolean isActive(String appName) {
521 return appFile(appName, "active").exists();
522 }
523
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800524 // Returns the name of the file located under the specified app directory.
525 private File appFile(String appName, String fileName) {
Ray Milkey74c98a32018-07-26 10:05:49 -0700526 File file = new File(new File(appsDir, appName), fileName);
527 if (!FilePathValidator.validateFile(file, appsDir)) {
528 throw new ApplicationException("Application attempting to create files outside the apps directory");
529 }
530 return file;
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800531 }
532
Jian Li97d6b2d2016-01-20 10:13:43 -0800533 // Returns the icon file located under the specified app directory.
534 private File iconFile(String appName, String fileName) {
535 return new File(new File(appsDir, appName), fileName);
536 }
537
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900538 // Returns the set of Permissions specified in the app.xml file
539 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Thomas Vachuska761f0042015-11-11 19:10:17 -0800540 List<Permission> permissionList = Lists.newArrayList();
Changhoon Yoonb856b812015-08-10 03:47:19 +0900541
542 for (Object o : cfg.getList(APP_PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900543 String name = (String) o;
Changhoon Yoonb856b812015-08-10 03:47:19 +0900544 permissionList.add(new Permission(AppPermission.class.getName(), name));
545 }
546 for (Object o : cfg.getList(NET_PERMISSIONS)) {
547 //TODO: TO BE FLESHED OUT WHEN NETWORK PERMISSIONS ARE SUPPORTED
548 break;
549 }
550
551 List<HierarchicalConfiguration> fields =
552 cfg.configurationsAt(JAVA_PERMISSIONS);
553 for (HierarchicalConfiguration sub : fields) {
554 String classname = sub.getString("classname");
555 String name = sub.getString("name");
556 String actions = sub.getString("actions");
557
558 if (classname != null && name != null) {
559 permissionList.add(new Permission(classname, name, actions));
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900560 }
561 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900562 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900563 }
564
Jian Lic35415d2016-01-14 17:22:31 -0800565 // Returns the byte stream from icon.png file in oar application archive.
566 private byte[] getApplicationIcon(String appName) {
Jian Li97d6b2d2016-01-20 10:13:43 -0800567 File iconFile = iconFile(appName, APP_PNG);
Jian Lic35415d2016-01-14 17:22:31 -0800568 try {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700569 final InputStream iconStream;
570 if (iconFile.exists()) {
571 iconStream = new FileInputStream(iconFile);
572 } else {
573 // assume that we can always fallback to default icon
574 iconStream = ApplicationArchive.class.getResourceAsStream("/" + APP_PNG);
575 }
柯志勇10068695d64ea7b2018-10-19 14:54:52 +0800576 byte[] icon = ByteStreams.toByteArray(iconStream);
577 iconStream.close();
578 return icon;
Jian Lic35415d2016-01-14 17:22:31 -0800579 } catch (IOException e) {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700580 log.warn("Unable to read app icon for app {}", appName, e);
Jian Lic35415d2016-01-14 17:22:31 -0800581 }
Brian O'Connora7903ae2016-04-29 15:02:32 -0700582 return new byte[0];
Jian Lic35415d2016-01-14 17:22:31 -0800583 }
584
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900585 // Returns application role type
586 public ApplicationRole getRole(String value) {
587 if (value == null) {
588 return ApplicationRole.UNSPECIFIED;
589 } else {
590 try {
591 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
592 } catch (IllegalArgumentException e) {
593 log.debug("Unknown role value: %s", value);
594 return ApplicationRole.UNSPECIFIED;
595 }
596 }
597 }
Jian Lic35415d2016-01-14 17:22:31 -0800598
599 // Returns the first sentence of the given sentence
600 private String compactDescription(String sentence) {
601 if (StringUtils.isNotEmpty(sentence)) {
602 if (StringUtils.contains(sentence, ".")) {
603 return StringUtils.substringBefore(sentence, ".") + ".";
604 } else {
Jian Li8bcb4f22016-01-20 10:36:18 -0800605 return sentence;
Jian Lic35415d2016-01-14 17:22:31 -0800606 }
607 }
608 return sentence;
609 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800610}