blob: 339e68e9e53f2dcb77dd504006ce6c2bf408196a [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;
28import org.onosproject.app.ApplicationDescription;
29import org.onosproject.app.ApplicationEvent;
30import org.onosproject.app.ApplicationException;
31import org.onosproject.app.ApplicationStoreDelegate;
32import org.onosproject.app.DefaultApplicationDescription;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090033import org.onosproject.core.ApplicationRole;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080034import org.onosproject.core.Version;
Changhoon Yoonb856b812015-08-10 03:47:19 +090035import org.onosproject.security.AppPermission;
36import org.onosproject.security.Permission;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080037import org.onosproject.store.AbstractStore;
38import org.slf4j.Logger;
39import org.slf4j.LoggerFactory;
40
41import java.io.ByteArrayInputStream;
42import java.io.File;
43import java.io.FileInputStream;
44import java.io.FileNotFoundException;
45import java.io.IOException;
46import java.io.InputStream;
47import java.net.URI;
HIGUCHI Yuta436f8d52015-12-07 21:17:48 -080048import java.nio.charset.StandardCharsets;
Thomas Vachuska90b453f2015-01-30 18:57:14 -080049import java.nio.file.NoSuchFileException;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080050import java.util.List;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090051import java.util.Locale;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080052import java.util.Set;
53import java.util.zip.ZipEntry;
54import java.util.zip.ZipInputStream;
55
Thomas Vachuskae18a3302015-06-23 12:48:28 -070056import static com.google.common.base.Preconditions.checkState;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080057import static com.google.common.io.ByteStreams.toByteArray;
58import static com.google.common.io.Files.createParentDirs;
59import static com.google.common.io.Files.write;
60
61/**
62 * Facility for reading application archive stream and managing application
63 * directory structure.
64 */
65public class ApplicationArchive
66 extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
67
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070068 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
69
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080070 // Magic strings to search for at the beginning of the archive stream
71 private static final String XML_MAGIC = "<?xml ";
Brian O'Connore4a4f992016-04-06 22:50:31 -070072 private static final String ZIP_MAGIC = "PK";
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080073
74 // Magic strings to search for and how deep to search it into the archive stream
75 private static final String APP_MAGIC = "<app ";
76 private static final int APP_MAGIC_DEPTH = 1024;
77
Thomas Vachuska02aeb032015-01-06 22:36:30 -080078 private static final String NAME = "[@name]";
79 private static final String ORIGIN = "[@origin]";
80 private static final String VERSION = "[@version]";
81 private static final String FEATURES_REPO = "[@featuresRepo]";
82 private static final String FEATURES = "[@features]";
Thomas Vachuska761f0042015-11-11 19:10:17 -080083 private static final String APPS = "[@apps]";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080084 private static final String DESCRIPTION = "description";
85
Jian Li01b0f5952016-01-20 11:02:07 -080086 private static final String UTILITY = "utility";
87
Jian Lic35415d2016-01-14 17:22:31 -080088 private static final String CATEGORY = "[@category]";
89 private static final String URL = "[@url]";
Simon Huntafae2f72016-03-04 21:18:23 -080090 private static final String TITLE = "[@title]";
Jian Lic35415d2016-01-14 17:22:31 -080091
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090092 private static final String ROLE = "security.role";
Changhoon Yoonb856b812015-08-10 03:47:19 +090093 private static final String APP_PERMISSIONS = "security.permissions.app-perm";
94 private static final String NET_PERMISSIONS = "security.permissions.net-perm";
95 private static final String JAVA_PERMISSIONS = "security.permissions.java-perm";
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090096
Thomas Vachuska6cc74882017-01-10 16:01:02 -080097 private static final String JAR = ".jar";
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070098 private static final String OAR = ".oar";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080099 private static final String APP_XML = "app.xml";
Jian Li97d6b2d2016-01-20 10:13:43 -0800100 private static final String APP_PNG = "app.png";
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800101 private static final String M2_PREFIX = "m2";
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800102 private static final String FEATURES_XML = "features.xml";
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800103
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700104 private static final String ROOT = "../";
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800105 private static final String M2_ROOT = "system/";
Thomas Vachuskad5d9bcb2015-03-18 17:46:20 -0700106 private static final String APPS_ROOT = "apps/";
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800107
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700108 private File root = new File(ROOT);
109 private File appsDir = new File(root, APPS_ROOT);
110 private File m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800111
112 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700113 * Sets the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800114 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700115 * @param root top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800116 */
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700117 protected void setRootPath(String root) {
118 this.root = new File(root);
119 this.appsDir = new File(this.root, APPS_ROOT);
120 this.m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800121 }
122
123 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700124 * Returns the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800125 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700126 * @return top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800127 */
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700128 public String getRootPath() {
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700129 return root.getPath();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800130 }
131
132 /**
133 * Returns the set of installed application names.
134 *
135 * @return installed application names
136 */
137 public Set<String> getApplicationNames() {
138 ImmutableSet.Builder<String> names = ImmutableSet.builder();
139 File[] files = appsDir.listFiles(File::isDirectory);
140 if (files != null) {
141 for (File file : files) {
142 names.add(file.getName());
143 }
144 }
145 return names.build();
146 }
147
148 /**
Thomas Vachuskacf960112015-03-06 22:36:51 -0800149 * Returns the timestamp in millis since start of epoch, of when the
150 * specified application was last modified or changed state.
151 *
152 * @param appName application name
153 * @return number of millis since start of epoch
154 */
155 public long getUpdateTime(String appName) {
156 return appFile(appName, APP_XML).lastModified();
157 }
158
159 /**
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800160 * Loads the application descriptor from the specified application archive
161 * stream and saves the stream in the appropriate application archive
162 * directory.
163 *
164 * @param appName application name
165 * @return application descriptor
166 * @throws org.onosproject.app.ApplicationException if unable to read application description
167 */
168 public ApplicationDescription getApplicationDescription(String appName) {
169 try {
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700170 XMLConfiguration cfg = new XMLConfiguration();
171 cfg.setAttributeSplittingDisabled(true);
172 cfg.setDelimiterParsingDisabled(true);
173 cfg.load(appFile(appName, APP_XML));
174 return loadAppDescription(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800175 } catch (Exception e) {
176 throw new ApplicationException("Unable to get app description", e);
177 }
178 }
179
180 /**
181 * Loads the application descriptor from the specified application archive
182 * stream and saves the stream in the appropriate application archive
183 * directory.
184 *
185 * @param stream application archive stream
186 * @return application descriptor
187 * @throws org.onosproject.app.ApplicationException if unable to read the
188 * archive stream or store
189 * the application archive
190 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800191 public synchronized ApplicationDescription saveApplication(InputStream stream) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800192 try (InputStream ais = stream) {
193 byte[] cache = toByteArray(ais);
194 InputStream bis = new ByteArrayInputStream(cache);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800195
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800196 boolean plainXml = isPlainXml(cache);
197 ApplicationDescription desc = plainXml ?
198 parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700199 checkState(!appFile(desc.name(), APP_XML).exists(),
Jian Li97d6b2d2016-01-20 10:13:43 -0800200 "Application %s already installed", desc.name());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800201
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800202 if (plainXml) {
203 expandPlainApplication(cache, desc);
204 } else {
205 bis.reset();
Thomas Vachuska08b4dec2017-08-31 15:20:17 -0700206 boolean isSelfContainedJar = expandZippedApplication(bis, desc);
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800207
208 if (isSelfContainedJar) {
209 bis.reset();
210 stageSelfContainedJar(bis, desc);
211 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800212
213 bis.reset();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800214 saveApplication(bis, desc, isSelfContainedJar);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800215 }
216
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800217 installArtifacts(desc);
218 return desc;
219 } catch (IOException e) {
220 throw new ApplicationException("Unable to save application", e);
221 }
222 }
223
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800224 // Indicates whether the stream encoded in the given bytes is plain XML.
225 private boolean isPlainXml(byte[] bytes) {
Brian O'Connore4a4f992016-04-06 22:50:31 -0700226 return !substring(bytes, ZIP_MAGIC.length()).equals(ZIP_MAGIC) &&
227 (substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
228 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC));
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800229 }
230
231 // Returns the substring of maximum possible length from the specified bytes.
232 private String substring(byte[] bytes, int length) {
HIGUCHI Yuta436f8d52015-12-07 21:17:48 -0800233 return new String(bytes, 0, Math.min(bytes.length, length), StandardCharsets.UTF_8);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800234 }
235
Ray Milkey4b19da62018-07-08 10:06:19 -0700236 private String filterAppNameForFilesystem(String name) {
237 return name.replace("/", "^");
238 }
239
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800240 /**
241 * Purges the application archive directory.
242 *
243 * @param appName application name
244 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800245 public synchronized void purgeApplication(String appName) {
Ray Milkey4b19da62018-07-08 10:06:19 -0700246 File appDir = new File(appsDir, filterAppNameForFilesystem(appName));
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 Milkey4b19da62018-07-08 10:06:19 -0700360 File appDir = new File(appsDir, filterAppNameForFilesystem(desc.name()));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800361 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800362 if (!entry.isDirectory()) {
363 byte[] data = ByteStreams.toByteArray(zis);
364 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800365 File file = new File(appDir, entry.getName());
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800366 if (isTopLevel(file)) {
367 createParentDirs(file);
368 write(data, file);
369 } else {
370 isSelfContained = true;
371 }
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800372 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800373 }
374 zis.close();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800375 return isSelfContained;
376 }
377
378 // Returns true if the specified file is a top-level app file, i.e. app.xml,
379 // features.xml, .jar or a directory; false if anything else.
380 private boolean isTopLevel(File file) {
381 String name = file.getName();
382 return name.equals(APP_XML) || name.endsWith(FEATURES_XML) || name.endsWith(JAR) || file.isDirectory();
383 }
384
385 // Expands the self-contained JAR stream into the app-specific directory,
386 // using the bundle coordinates retrieved from the features.xml file.
387 private void stageSelfContainedJar(InputStream stream, ApplicationDescription desc)
388 throws IOException {
389 // First extract the bundle coordinates
390 String coords = getSelfContainedBundleCoordinates(desc);
391 if (coords == null) {
392 return;
393 }
394
395 // Split the coordinates into segments and build the file name.
396 String[] f = coords.substring(4).split("/");
397 String base = "m2/" + f[0].replace('.', '/') + "/" + f[1] + "/" + f[2] + "/" + f[1] + "-" + f[2];
398 String jarName = base + (f.length < 4 ? "" : "-" + f[3]) + ".jar";
399 String featuresName = base + "-features.xml";
400
401 // Create the file directory structure and copy the file there.
402 File jar = appFile(desc.name(), jarName);
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700403 boolean ok = jar.getParentFile().exists() || jar.getParentFile().mkdirs();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800404 if (ok) {
405 Files.write(toByteArray(stream), jar);
406 Files.copy(appFile(desc.name(), FEATURES_XML), appFile(desc.name(), featuresName));
407 if (!appFile(desc.name(), FEATURES_XML).delete()) {
408 log.warn("Unable to delete self-contained application {} features.xml", desc.name());
409 }
410 } else {
411 throw new IOException("Unable to save self-contained application " + desc.name());
412 }
413 }
414
415 // Returns the bundle coordinates from the features.xml file.
416 private String getSelfContainedBundleCoordinates(ApplicationDescription desc) {
417 try {
418 XMLConfiguration cfg = new XMLConfiguration();
419 cfg.setAttributeSplittingDisabled(true);
420 cfg.setDelimiterParsingDisabled(true);
421 cfg.load(appFile(desc.name(), FEATURES_XML));
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700422 return cfg.getString("feature.bundle")
423 .replaceFirst("wrap:", "")
424 .replaceFirst("\\$Bundle-.*$", "");
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800425 } catch (ConfigurationException e) {
426 log.warn("Self-contained application {} has no features.xml", desc.name());
427 return null;
428 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800429 }
430
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800431 // Saves the specified XML stream into app-specific directory.
432 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
433 throws IOException {
434 File file = appFile(desc.name(), APP_XML);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700435 checkState(!file.getParentFile().exists(), "Application already installed");
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800436 createParentDirs(file);
437 write(stream, file);
438 }
439
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800440 // Saves the specified ZIP stream into a file under app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800441 private void saveApplication(InputStream stream, ApplicationDescription desc,
442 boolean isSelfContainedJar)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800443 throws IOException {
Ray Milkey4b19da62018-07-08 10:06:19 -0700444 String name = filterAppNameForFilesystem(desc.name()) + (isSelfContainedJar ? JAR : OAR);
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800445 Files.write(toByteArray(stream), appFile(desc.name(), name));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800446 }
447
448 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800449 private void installArtifacts(ApplicationDescription desc) throws IOException {
450 try {
451 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
452 } catch (NoSuchFileException e) {
453 log.debug("Application {} has no M2 artifacts", desc.name());
454 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800455 }
456
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800457 /**
458 * Marks the app as active by creating token file in the app directory.
459 *
460 * @param appName application name
461 * @return true if file was created
462 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800463 protected boolean setActive(String appName) {
464 try {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800465 File active = appFile(appName, "active");
466 createParentDirs(active);
467 return active.createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800468 } catch (IOException e) {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800469 log.warn("Unable to mark app {} as active", appName, e);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800470 throw new ApplicationException("Unable to mark app as active", e);
471 }
472 }
473
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800474 /**
475 * Clears the app as active by deleting token file in the app directory.
476 *
477 * @param appName application name
478 * @return true if file was deleted
479 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800480 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800481 return appFile(appName, "active").delete() && updateTime(appName);
482 }
483
484 /**
485 * Updates the time-stamp of the app descriptor file.
486 *
487 * @param appName application name
488 * @return true if the app descriptor was updated
489 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700490 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800491 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800492 }
493
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800494 /**
495 * Indicates whether the app was marked as active by checking for token file.
496 *
497 * @param appName application name
498 * @return true if the app is marked as active
499 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800500 protected boolean isActive(String appName) {
501 return appFile(appName, "active").exists();
502 }
503
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800504 // Returns the name of the file located under the specified app directory.
505 private File appFile(String appName, String fileName) {
Ray Milkey4b19da62018-07-08 10:06:19 -0700506 return new File(new File(appsDir, filterAppNameForFilesystem(appName)), fileName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800507 }
508
Jian Li97d6b2d2016-01-20 10:13:43 -0800509 // Returns the icon file located under the specified app directory.
510 private File iconFile(String appName, String fileName) {
511 return new File(new File(appsDir, appName), fileName);
512 }
513
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900514 // Returns the set of Permissions specified in the app.xml file
515 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Thomas Vachuska761f0042015-11-11 19:10:17 -0800516 List<Permission> permissionList = Lists.newArrayList();
Changhoon Yoonb856b812015-08-10 03:47:19 +0900517
518 for (Object o : cfg.getList(APP_PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900519 String name = (String) o;
Changhoon Yoonb856b812015-08-10 03:47:19 +0900520 permissionList.add(new Permission(AppPermission.class.getName(), name));
521 }
522 for (Object o : cfg.getList(NET_PERMISSIONS)) {
523 //TODO: TO BE FLESHED OUT WHEN NETWORK PERMISSIONS ARE SUPPORTED
524 break;
525 }
526
527 List<HierarchicalConfiguration> fields =
528 cfg.configurationsAt(JAVA_PERMISSIONS);
529 for (HierarchicalConfiguration sub : fields) {
530 String classname = sub.getString("classname");
531 String name = sub.getString("name");
532 String actions = sub.getString("actions");
533
534 if (classname != null && name != null) {
535 permissionList.add(new Permission(classname, name, actions));
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900536 }
537 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900538 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900539 }
540
Jian Lic35415d2016-01-14 17:22:31 -0800541 // Returns the byte stream from icon.png file in oar application archive.
542 private byte[] getApplicationIcon(String appName) {
Jian Li97d6b2d2016-01-20 10:13:43 -0800543 File iconFile = iconFile(appName, APP_PNG);
Jian Lic35415d2016-01-14 17:22:31 -0800544 try {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700545 final InputStream iconStream;
546 if (iconFile.exists()) {
547 iconStream = new FileInputStream(iconFile);
548 } else {
549 // assume that we can always fallback to default icon
550 iconStream = ApplicationArchive.class.getResourceAsStream("/" + APP_PNG);
551 }
552 return ByteStreams.toByteArray(iconStream);
Jian Lic35415d2016-01-14 17:22:31 -0800553 } catch (IOException e) {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700554 log.warn("Unable to read app icon for app {}", appName, e);
Jian Lic35415d2016-01-14 17:22:31 -0800555 }
Brian O'Connora7903ae2016-04-29 15:02:32 -0700556 return new byte[0];
Jian Lic35415d2016-01-14 17:22:31 -0800557 }
558
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900559 // Returns application role type
560 public ApplicationRole getRole(String value) {
561 if (value == null) {
562 return ApplicationRole.UNSPECIFIED;
563 } else {
564 try {
565 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
566 } catch (IllegalArgumentException e) {
567 log.debug("Unknown role value: %s", value);
568 return ApplicationRole.UNSPECIFIED;
569 }
570 }
571 }
Jian Lic35415d2016-01-14 17:22:31 -0800572
573 // Returns the first sentence of the given sentence
574 private String compactDescription(String sentence) {
575 if (StringUtils.isNotEmpty(sentence)) {
576 if (StringUtils.contains(sentence, ".")) {
577 return StringUtils.substringBefore(sentence, ".") + ".";
578 } else {
Jian Li8bcb4f22016-01-20 10:36:18 -0800579 return sentence;
Jian Lic35415d2016-01-14 17:22:31 -0800580 }
581 }
582 return sentence;
583 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800584}