blob: 92709cbd1082d089d2efe47ccc3960b231cf0b30 [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
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800236 /**
237 * Purges the application archive directory.
238 *
239 * @param appName application name
240 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800241 public synchronized void purgeApplication(String appName) {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800242 File appDir = new File(appsDir, appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800243 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800244 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800245 } catch (IOException e) {
246 throw new ApplicationException("Unable to purge application " + appName, e);
247 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800248 if (appDir.exists()) {
249 throw new ApplicationException("Unable to purge application " + appName);
250 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800251 }
252
253 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800254 * Returns application archive stream for the specified application. This
Thomas Vachuska08b4dec2017-08-31 15:20:17 -0700255 * will be either the application OAR file, JAR file or the plain XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800256 *
257 * @param appName application name
258 * @return application archive stream
259 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800260 public synchronized InputStream getApplicationInputStream(String appName) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800261 try {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700262 File appFile = appFile(appName, appName + OAR);
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700263 if (!appFile.exists()) {
264 appFile = appFile(appName, appName + JAR);
265 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800266 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800267 } catch (FileNotFoundException e) {
268 throw new ApplicationException("Application " + appName + " not found");
269 }
270 }
271
272 // Scans the specified ZIP stream for app.xml entry and parses it producing
273 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800274 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800275 throws IOException {
276 try (ZipInputStream zis = new ZipInputStream(stream)) {
277 ZipEntry entry;
278 while ((entry = zis.getNextEntry()) != null) {
279 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800280 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800281 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800282 }
283 zis.closeEntry();
284 }
285 }
286 throw new IOException("Unable to locate " + APP_XML);
287 }
288
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800289 // Scans the specified XML stream and parses it producing an application descriptor.
290 private ApplicationDescription parsePlainAppDescription(InputStream stream)
291 throws IOException {
292 XMLConfiguration cfg = new XMLConfiguration();
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700293 cfg.setAttributeSplittingDisabled(true);
294 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800295 try {
296 cfg.load(stream);
297 return loadAppDescription(cfg);
298 } catch (ConfigurationException e) {
299 throw new IOException("Unable to parse " + APP_XML, e);
300 }
301 }
302
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800303 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800304 String name = cfg.getString(NAME);
305 Version version = Version.version(cfg.getString(VERSION));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800306 String origin = cfg.getString(ORIGIN);
Simon Huntafae2f72016-03-04 21:18:23 -0800307
308 String title = cfg.getString(TITLE);
309 // FIXME: title should be set as attribute to APP, but fallback for now...
310 title = title == null ? name : title;
311
Jian Li01b0f5952016-01-20 11:02:07 -0800312 String category = cfg.getString(CATEGORY, UTILITY);
Jian Lic35415d2016-01-14 17:22:31 -0800313 String url = cfg.getString(URL);
314 byte[] icon = getApplicationIcon(name);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900315 ApplicationRole role = getRole(cfg.getString(ROLE));
316 Set<Permission> perms = getPermissions(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800317 String featRepo = cfg.getString(FEATURES_REPO);
318 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700319 List<String> features = ImmutableList.copyOf(cfg.getString(FEATURES).split(","));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800320
Thomas Vachuska761f0042015-11-11 19:10:17 -0800321 String apps = cfg.getString(APPS, "");
322 List<String> requiredApps = apps.isEmpty() ?
323 ImmutableList.of() : ImmutableList.copyOf(apps.split(","));
324
Jian Li8bcb4f22016-01-20 10:36:18 -0800325 // put full description to readme field
326 String readme = cfg.getString(DESCRIPTION);
Jian Lic35415d2016-01-14 17:22:31 -0800327
Jian Li8bcb4f22016-01-20 10:36:18 -0800328 // put short description to description field
329 String desc = compactDescription(readme);
Jian Lic35415d2016-01-14 17:22:31 -0800330
Simon Huntafae2f72016-03-04 21:18:23 -0800331 return new DefaultApplicationDescription(name, version, title, desc, origin,
Jian Li8bcb4f22016-01-20 10:36:18 -0800332 category, url, readme, icon,
333 role, perms, featuresRepo,
334 features, requiredApps);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800335 }
336
337 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800338 // Returns true of the application is a self-contained jar rather than an oar file.
339 private boolean expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800340 throws IOException {
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800341 boolean isSelfContained = false;
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800342 ZipInputStream zis = new ZipInputStream(stream);
343 ZipEntry entry;
344 File appDir = new File(appsDir, desc.name());
345 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800346 if (!entry.isDirectory()) {
347 byte[] data = ByteStreams.toByteArray(zis);
348 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800349 File file = new File(appDir, entry.getName());
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800350 if (isTopLevel(file)) {
351 createParentDirs(file);
352 write(data, file);
353 } else {
354 isSelfContained = true;
355 }
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800356 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800357 }
358 zis.close();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800359 return isSelfContained;
360 }
361
362 // Returns true if the specified file is a top-level app file, i.e. app.xml,
363 // features.xml, .jar or a directory; false if anything else.
364 private boolean isTopLevel(File file) {
365 String name = file.getName();
366 return name.equals(APP_XML) || name.endsWith(FEATURES_XML) || name.endsWith(JAR) || file.isDirectory();
367 }
368
369 // Expands the self-contained JAR stream into the app-specific directory,
370 // using the bundle coordinates retrieved from the features.xml file.
371 private void stageSelfContainedJar(InputStream stream, ApplicationDescription desc)
372 throws IOException {
373 // First extract the bundle coordinates
374 String coords = getSelfContainedBundleCoordinates(desc);
375 if (coords == null) {
376 return;
377 }
378
379 // Split the coordinates into segments and build the file name.
380 String[] f = coords.substring(4).split("/");
381 String base = "m2/" + f[0].replace('.', '/') + "/" + f[1] + "/" + f[2] + "/" + f[1] + "-" + f[2];
382 String jarName = base + (f.length < 4 ? "" : "-" + f[3]) + ".jar";
383 String featuresName = base + "-features.xml";
384
385 // Create the file directory structure and copy the file there.
386 File jar = appFile(desc.name(), jarName);
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700387 boolean ok = jar.getParentFile().exists() || jar.getParentFile().mkdirs();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800388 if (ok) {
389 Files.write(toByteArray(stream), jar);
390 Files.copy(appFile(desc.name(), FEATURES_XML), appFile(desc.name(), featuresName));
391 if (!appFile(desc.name(), FEATURES_XML).delete()) {
392 log.warn("Unable to delete self-contained application {} features.xml", desc.name());
393 }
394 } else {
395 throw new IOException("Unable to save self-contained application " + desc.name());
396 }
397 }
398
399 // Returns the bundle coordinates from the features.xml file.
400 private String getSelfContainedBundleCoordinates(ApplicationDescription desc) {
401 try {
402 XMLConfiguration cfg = new XMLConfiguration();
403 cfg.setAttributeSplittingDisabled(true);
404 cfg.setDelimiterParsingDisabled(true);
405 cfg.load(appFile(desc.name(), FEATURES_XML));
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700406 return cfg.getString("feature.bundle")
407 .replaceFirst("wrap:", "")
408 .replaceFirst("\\$Bundle-.*$", "");
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800409 } catch (ConfigurationException e) {
410 log.warn("Self-contained application {} has no features.xml", desc.name());
411 return null;
412 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800413 }
414
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800415 // Saves the specified XML stream into app-specific directory.
416 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
417 throws IOException {
418 File file = appFile(desc.name(), APP_XML);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700419 checkState(!file.getParentFile().exists(), "Application already installed");
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800420 createParentDirs(file);
421 write(stream, file);
422 }
423
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800424 // Saves the specified ZIP stream into a file under app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800425 private void saveApplication(InputStream stream, ApplicationDescription desc,
426 boolean isSelfContainedJar)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800427 throws IOException {
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800428 String name = desc.name() + (isSelfContainedJar ? JAR : OAR);
429 Files.write(toByteArray(stream), appFile(desc.name(), name));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800430 }
431
432 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800433 private void installArtifacts(ApplicationDescription desc) throws IOException {
434 try {
435 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
436 } catch (NoSuchFileException e) {
437 log.debug("Application {} has no M2 artifacts", desc.name());
438 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800439 }
440
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800441 /**
442 * Marks the app as active by creating token file in the app directory.
443 *
444 * @param appName application name
445 * @return true if file was created
446 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800447 protected boolean setActive(String appName) {
448 try {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800449 File active = appFile(appName, "active");
450 createParentDirs(active);
451 return active.createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800452 } catch (IOException e) {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800453 log.warn("Unable to mark app {} as active", appName, e);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800454 throw new ApplicationException("Unable to mark app as active", e);
455 }
456 }
457
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800458 /**
459 * Clears the app as active by deleting token file in the app directory.
460 *
461 * @param appName application name
462 * @return true if file was deleted
463 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800464 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800465 return appFile(appName, "active").delete() && updateTime(appName);
466 }
467
468 /**
469 * Updates the time-stamp of the app descriptor file.
470 *
471 * @param appName application name
472 * @return true if the app descriptor was updated
473 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700474 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800475 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800476 }
477
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800478 /**
479 * Indicates whether the app was marked as active by checking for token file.
480 *
481 * @param appName application name
482 * @return true if the app is marked as active
483 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800484 protected boolean isActive(String appName) {
485 return appFile(appName, "active").exists();
486 }
487
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800488 // Returns the name of the file located under the specified app directory.
489 private File appFile(String appName, String fileName) {
490 return new File(new File(appsDir, appName), fileName);
491 }
492
Jian Li97d6b2d2016-01-20 10:13:43 -0800493 // Returns the icon file located under the specified app directory.
494 private File iconFile(String appName, String fileName) {
495 return new File(new File(appsDir, appName), fileName);
496 }
497
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900498 // Returns the set of Permissions specified in the app.xml file
499 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Thomas Vachuska761f0042015-11-11 19:10:17 -0800500 List<Permission> permissionList = Lists.newArrayList();
Changhoon Yoonb856b812015-08-10 03:47:19 +0900501
502 for (Object o : cfg.getList(APP_PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900503 String name = (String) o;
Changhoon Yoonb856b812015-08-10 03:47:19 +0900504 permissionList.add(new Permission(AppPermission.class.getName(), name));
505 }
506 for (Object o : cfg.getList(NET_PERMISSIONS)) {
507 //TODO: TO BE FLESHED OUT WHEN NETWORK PERMISSIONS ARE SUPPORTED
508 break;
509 }
510
511 List<HierarchicalConfiguration> fields =
512 cfg.configurationsAt(JAVA_PERMISSIONS);
513 for (HierarchicalConfiguration sub : fields) {
514 String classname = sub.getString("classname");
515 String name = sub.getString("name");
516 String actions = sub.getString("actions");
517
518 if (classname != null && name != null) {
519 permissionList.add(new Permission(classname, name, actions));
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900520 }
521 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900522 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900523 }
524
Jian Lic35415d2016-01-14 17:22:31 -0800525 // Returns the byte stream from icon.png file in oar application archive.
526 private byte[] getApplicationIcon(String appName) {
Jian Li97d6b2d2016-01-20 10:13:43 -0800527 File iconFile = iconFile(appName, APP_PNG);
Jian Lic35415d2016-01-14 17:22:31 -0800528 try {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700529 final InputStream iconStream;
530 if (iconFile.exists()) {
531 iconStream = new FileInputStream(iconFile);
532 } else {
533 // assume that we can always fallback to default icon
534 iconStream = ApplicationArchive.class.getResourceAsStream("/" + APP_PNG);
535 }
536 return ByteStreams.toByteArray(iconStream);
Jian Lic35415d2016-01-14 17:22:31 -0800537 } catch (IOException e) {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700538 log.warn("Unable to read app icon for app {}", appName, e);
Jian Lic35415d2016-01-14 17:22:31 -0800539 }
Brian O'Connora7903ae2016-04-29 15:02:32 -0700540 return new byte[0];
Jian Lic35415d2016-01-14 17:22:31 -0800541 }
542
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900543 // Returns application role type
544 public ApplicationRole getRole(String value) {
545 if (value == null) {
546 return ApplicationRole.UNSPECIFIED;
547 } else {
548 try {
549 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
550 } catch (IllegalArgumentException e) {
551 log.debug("Unknown role value: %s", value);
552 return ApplicationRole.UNSPECIFIED;
553 }
554 }
555 }
Jian Lic35415d2016-01-14 17:22:31 -0800556
557 // Returns the first sentence of the given sentence
558 private String compactDescription(String sentence) {
559 if (StringUtils.isNotEmpty(sentence)) {
560 if (StringUtils.contains(sentence, ".")) {
561 return StringUtils.substringBefore(sentence, ".") + ".";
562 } else {
Jian Li8bcb4f22016-01-20 10:36:18 -0800563 return sentence;
Jian Lic35415d2016-01-14 17:22:31 -0800564 }
565 }
566 return sentence;
567 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800568}