blob: 13cc40366569d39315afde2ac538b7e56c13a7eb [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
214 bis.reset();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800215 saveApplication(bis, desc, isSelfContainedJar);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800216 }
217
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800218 installArtifacts(desc);
219 return desc;
220 } catch (IOException e) {
221 throw new ApplicationException("Unable to save application", e);
222 }
223 }
224
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800225 // Indicates whether the stream encoded in the given bytes is plain XML.
226 private boolean isPlainXml(byte[] bytes) {
Brian O'Connore4a4f992016-04-06 22:50:31 -0700227 return !substring(bytes, ZIP_MAGIC.length()).equals(ZIP_MAGIC) &&
228 (substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
229 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC));
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800230 }
231
232 // Returns the substring of maximum possible length from the specified bytes.
233 private String substring(byte[] bytes, int length) {
HIGUCHI Yuta436f8d52015-12-07 21:17:48 -0800234 return new String(bytes, 0, Math.min(bytes.length, length), StandardCharsets.UTF_8);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800235 }
236
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800237 /**
238 * Purges the application archive directory.
239 *
240 * @param appName application name
241 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800242 public synchronized void purgeApplication(String appName) {
Ray Milkey74c98a32018-07-26 10:05:49 -0700243 File appDir = new File(appsDir, appName);
244 if (!FilePathValidator.validateFile(appDir, appsDir)) {
245 throw new ApplicationException("Application attempting to create files outside the apps directory");
246 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800247 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800248 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800249 } catch (IOException e) {
250 throw new ApplicationException("Unable to purge application " + appName, e);
251 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800252 if (appDir.exists()) {
253 throw new ApplicationException("Unable to purge application " + appName);
254 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800255 }
256
257 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800258 * Returns application archive stream for the specified application. This
Thomas Vachuska08b4dec2017-08-31 15:20:17 -0700259 * will be either the application OAR file, JAR file or the plain XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800260 *
261 * @param appName application name
262 * @return application archive stream
263 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800264 public synchronized InputStream getApplicationInputStream(String appName) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800265 try {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700266 File appFile = appFile(appName, appName + OAR);
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700267 if (!appFile.exists()) {
268 appFile = appFile(appName, appName + JAR);
269 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800270 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800271 } catch (FileNotFoundException e) {
272 throw new ApplicationException("Application " + appName + " not found");
273 }
274 }
275
276 // Scans the specified ZIP stream for app.xml entry and parses it producing
277 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800278 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800279 throws IOException {
280 try (ZipInputStream zis = new ZipInputStream(stream)) {
281 ZipEntry entry;
282 while ((entry = zis.getNextEntry()) != null) {
283 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800284 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800285 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800286 }
287 zis.closeEntry();
288 }
289 }
290 throw new IOException("Unable to locate " + APP_XML);
291 }
292
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800293 // Scans the specified XML stream and parses it producing an application descriptor.
294 private ApplicationDescription parsePlainAppDescription(InputStream stream)
295 throws IOException {
296 XMLConfiguration cfg = new XMLConfiguration();
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700297 cfg.setAttributeSplittingDisabled(true);
298 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800299 try {
300 cfg.load(stream);
301 return loadAppDescription(cfg);
302 } catch (ConfigurationException e) {
303 throw new IOException("Unable to parse " + APP_XML, e);
304 }
305 }
306
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800307 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800308 String name = cfg.getString(NAME);
309 Version version = Version.version(cfg.getString(VERSION));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800310 String origin = cfg.getString(ORIGIN);
Simon Huntafae2f72016-03-04 21:18:23 -0800311
312 String title = cfg.getString(TITLE);
313 // FIXME: title should be set as attribute to APP, but fallback for now...
314 title = title == null ? name : title;
315
Jian Li01b0f5952016-01-20 11:02:07 -0800316 String category = cfg.getString(CATEGORY, UTILITY);
Jian Lic35415d2016-01-14 17:22:31 -0800317 String url = cfg.getString(URL);
318 byte[] icon = getApplicationIcon(name);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900319 ApplicationRole role = getRole(cfg.getString(ROLE));
320 Set<Permission> perms = getPermissions(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800321 String featRepo = cfg.getString(FEATURES_REPO);
322 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700323 List<String> features = ImmutableList.copyOf(cfg.getString(FEATURES).split(","));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800324
Thomas Vachuska761f0042015-11-11 19:10:17 -0800325 String apps = cfg.getString(APPS, "");
326 List<String> requiredApps = apps.isEmpty() ?
327 ImmutableList.of() : ImmutableList.copyOf(apps.split(","));
328
Jian Li8bcb4f22016-01-20 10:36:18 -0800329 // put full description to readme field
330 String readme = cfg.getString(DESCRIPTION);
Jian Lic35415d2016-01-14 17:22:31 -0800331
Jian Li8bcb4f22016-01-20 10:36:18 -0800332 // put short description to description field
333 String desc = compactDescription(readme);
Jian Lic35415d2016-01-14 17:22:31 -0800334
Ray Milkey47c95412017-09-15 10:40:48 -0700335 return DefaultApplicationDescription.builder()
336 .withName(name)
337 .withVersion(version)
338 .withTitle(title)
339 .withDescription(desc)
340 .withOrigin(origin)
341 .withCategory(category)
342 .withUrl(url)
343 .withReadme(readme)
344 .withIcon(icon)
345 .withRole(role)
346 .withPermissions(perms)
347 .withFeaturesRepo(featuresRepo)
348 .withFeatures(features)
349 .withRequiredApps(requiredApps)
350 .build();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800351 }
352
353 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800354 // Returns true of the application is a self-contained jar rather than an oar file.
355 private boolean expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800356 throws IOException {
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800357 boolean isSelfContained = false;
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800358 ZipInputStream zis = new ZipInputStream(stream);
359 ZipEntry entry;
Ray Milkey74c98a32018-07-26 10:05:49 -0700360 File appDir = new File(appsDir, desc.name());
361 if (!FilePathValidator.validateFile(appDir, appsDir)) {
362 throw new ApplicationException("Application attempting to create files outside the apps directory");
363 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800364 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800365 if (!entry.isDirectory()) {
366 byte[] data = ByteStreams.toByteArray(zis);
367 zis.closeEntry();
Ray Milkey74c98a32018-07-26 10:05:49 -0700368 if (FilePathValidator.validateZipEntry(entry, appDir)) {
Ray Milkey351d4562018-07-25 12:31:48 -0700369 File file = new File(appDir, entry.getName());
370 if (isTopLevel(file)) {
371 createParentDirs(file);
372 write(data, file);
373 } else {
374 isSelfContained = true;
375 }
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800376 } else {
Ray Milkey351d4562018-07-25 12:31:48 -0700377 throw new ApplicationException("Application Zip archive is attempting to leave application root");
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800378 }
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800379 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800380 }
381 zis.close();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800382 return isSelfContained;
383 }
384
385 // Returns true if the specified file is a top-level app file, i.e. app.xml,
386 // features.xml, .jar or a directory; false if anything else.
387 private boolean isTopLevel(File file) {
388 String name = file.getName();
389 return name.equals(APP_XML) || name.endsWith(FEATURES_XML) || name.endsWith(JAR) || file.isDirectory();
390 }
391
392 // Expands the self-contained JAR stream into the app-specific directory,
393 // using the bundle coordinates retrieved from the features.xml file.
394 private void stageSelfContainedJar(InputStream stream, ApplicationDescription desc)
395 throws IOException {
396 // First extract the bundle coordinates
397 String coords = getSelfContainedBundleCoordinates(desc);
398 if (coords == null) {
399 return;
400 }
401
402 // Split the coordinates into segments and build the file name.
403 String[] f = coords.substring(4).split("/");
404 String base = "m2/" + f[0].replace('.', '/') + "/" + f[1] + "/" + f[2] + "/" + f[1] + "-" + f[2];
405 String jarName = base + (f.length < 4 ? "" : "-" + f[3]) + ".jar";
406 String featuresName = base + "-features.xml";
407
408 // Create the file directory structure and copy the file there.
409 File jar = appFile(desc.name(), jarName);
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700410 boolean ok = jar.getParentFile().exists() || jar.getParentFile().mkdirs();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800411 if (ok) {
412 Files.write(toByteArray(stream), jar);
413 Files.copy(appFile(desc.name(), FEATURES_XML), appFile(desc.name(), featuresName));
414 if (!appFile(desc.name(), FEATURES_XML).delete()) {
415 log.warn("Unable to delete self-contained application {} features.xml", desc.name());
416 }
417 } else {
418 throw new IOException("Unable to save self-contained application " + desc.name());
419 }
420 }
421
422 // Returns the bundle coordinates from the features.xml file.
423 private String getSelfContainedBundleCoordinates(ApplicationDescription desc) {
424 try {
425 XMLConfiguration cfg = new XMLConfiguration();
426 cfg.setAttributeSplittingDisabled(true);
427 cfg.setDelimiterParsingDisabled(true);
428 cfg.load(appFile(desc.name(), FEATURES_XML));
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700429 return cfg.getString("feature.bundle")
430 .replaceFirst("wrap:", "")
431 .replaceFirst("\\$Bundle-.*$", "");
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800432 } catch (ConfigurationException e) {
433 log.warn("Self-contained application {} has no features.xml", desc.name());
434 return null;
435 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800436 }
437
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800438 // Saves the specified XML stream into app-specific directory.
439 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
440 throws IOException {
441 File file = appFile(desc.name(), APP_XML);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700442 checkState(!file.getParentFile().exists(), "Application already installed");
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800443 createParentDirs(file);
444 write(stream, file);
445 }
446
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800447 // Saves the specified ZIP stream into a file under app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800448 private void saveApplication(InputStream stream, ApplicationDescription desc,
449 boolean isSelfContainedJar)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800450 throws IOException {
Ray Milkey74c98a32018-07-26 10:05:49 -0700451 String name = desc.name() + (isSelfContainedJar ? JAR : OAR);
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800452 Files.write(toByteArray(stream), appFile(desc.name(), name));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800453 }
454
455 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800456 private void installArtifacts(ApplicationDescription desc) throws IOException {
457 try {
458 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
459 } catch (NoSuchFileException e) {
460 log.debug("Application {} has no M2 artifacts", desc.name());
461 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800462 }
463
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800464 /**
465 * Marks the app as active by creating token file in the app directory.
466 *
467 * @param appName application name
468 * @return true if file was created
469 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800470 protected boolean setActive(String appName) {
471 try {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800472 File active = appFile(appName, "active");
473 createParentDirs(active);
474 return active.createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800475 } catch (IOException e) {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800476 log.warn("Unable to mark app {} as active", appName, e);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800477 throw new ApplicationException("Unable to mark app as active", e);
478 }
479 }
480
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800481 /**
482 * Clears the app as active by deleting token file in the app directory.
483 *
484 * @param appName application name
485 * @return true if file was deleted
486 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800487 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800488 return appFile(appName, "active").delete() && updateTime(appName);
489 }
490
491 /**
492 * Updates the time-stamp of the app descriptor file.
493 *
494 * @param appName application name
495 * @return true if the app descriptor was updated
496 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700497 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800498 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800499 }
500
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800501 /**
502 * Indicates whether the app was marked as active by checking for token file.
503 *
504 * @param appName application name
505 * @return true if the app is marked as active
506 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800507 protected boolean isActive(String appName) {
508 return appFile(appName, "active").exists();
509 }
510
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800511 // Returns the name of the file located under the specified app directory.
512 private File appFile(String appName, String fileName) {
Ray Milkey74c98a32018-07-26 10:05:49 -0700513 File file = new File(new File(appsDir, appName), fileName);
514 if (!FilePathValidator.validateFile(file, appsDir)) {
515 throw new ApplicationException("Application attempting to create files outside the apps directory");
516 }
517 return file;
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800518 }
519
Jian Li97d6b2d2016-01-20 10:13:43 -0800520 // Returns the icon file located under the specified app directory.
521 private File iconFile(String appName, String fileName) {
522 return new File(new File(appsDir, appName), fileName);
523 }
524
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900525 // Returns the set of Permissions specified in the app.xml file
526 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Thomas Vachuska761f0042015-11-11 19:10:17 -0800527 List<Permission> permissionList = Lists.newArrayList();
Changhoon Yoonb856b812015-08-10 03:47:19 +0900528
529 for (Object o : cfg.getList(APP_PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900530 String name = (String) o;
Changhoon Yoonb856b812015-08-10 03:47:19 +0900531 permissionList.add(new Permission(AppPermission.class.getName(), name));
532 }
533 for (Object o : cfg.getList(NET_PERMISSIONS)) {
534 //TODO: TO BE FLESHED OUT WHEN NETWORK PERMISSIONS ARE SUPPORTED
535 break;
536 }
537
538 List<HierarchicalConfiguration> fields =
539 cfg.configurationsAt(JAVA_PERMISSIONS);
540 for (HierarchicalConfiguration sub : fields) {
541 String classname = sub.getString("classname");
542 String name = sub.getString("name");
543 String actions = sub.getString("actions");
544
545 if (classname != null && name != null) {
546 permissionList.add(new Permission(classname, name, actions));
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900547 }
548 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900549 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900550 }
551
Jian Lic35415d2016-01-14 17:22:31 -0800552 // Returns the byte stream from icon.png file in oar application archive.
553 private byte[] getApplicationIcon(String appName) {
Jian Li97d6b2d2016-01-20 10:13:43 -0800554 File iconFile = iconFile(appName, APP_PNG);
Jian Lic35415d2016-01-14 17:22:31 -0800555 try {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700556 final InputStream iconStream;
557 if (iconFile.exists()) {
558 iconStream = new FileInputStream(iconFile);
559 } else {
560 // assume that we can always fallback to default icon
561 iconStream = ApplicationArchive.class.getResourceAsStream("/" + APP_PNG);
562 }
563 return ByteStreams.toByteArray(iconStream);
Jian Lic35415d2016-01-14 17:22:31 -0800564 } catch (IOException e) {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700565 log.warn("Unable to read app icon for app {}", appName, e);
Jian Lic35415d2016-01-14 17:22:31 -0800566 }
Brian O'Connora7903ae2016-04-29 15:02:32 -0700567 return new byte[0];
Jian Lic35415d2016-01-14 17:22:31 -0800568 }
569
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900570 // Returns application role type
571 public ApplicationRole getRole(String value) {
572 if (value == null) {
573 return ApplicationRole.UNSPECIFIED;
574 } else {
575 try {
576 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
577 } catch (IllegalArgumentException e) {
578 log.debug("Unknown role value: %s", value);
579 return ApplicationRole.UNSPECIFIED;
580 }
581 }
582 }
Jian Lic35415d2016-01-14 17:22:31 -0800583
584 // Returns the first sentence of the given sentence
585 private String compactDescription(String sentence) {
586 if (StringUtils.isNotEmpty(sentence)) {
587 if (StringUtils.contains(sentence, ".")) {
588 return StringUtils.substringBefore(sentence, ".") + ".";
589 } else {
Jian Li8bcb4f22016-01-20 10:36:18 -0800590 return sentence;
Jian Lic35415d2016-01-14 17:22:31 -0800591 }
592 }
593 return sentence;
594 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800595}