blob: 55c198ed0358a7e94141c341bfe92d3ef970de56 [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
Ray Milkey47c95412017-09-15 10:40:48 -0700331 return DefaultApplicationDescription.builder()
332 .withName(name)
333 .withVersion(version)
334 .withTitle(title)
335 .withDescription(desc)
336 .withOrigin(origin)
337 .withCategory(category)
338 .withUrl(url)
339 .withReadme(readme)
340 .withIcon(icon)
341 .withRole(role)
342 .withPermissions(perms)
343 .withFeaturesRepo(featuresRepo)
344 .withFeatures(features)
345 .withRequiredApps(requiredApps)
346 .build();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800347 }
348
349 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800350 // Returns true of the application is a self-contained jar rather than an oar file.
351 private boolean expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800352 throws IOException {
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800353 boolean isSelfContained = false;
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800354 ZipInputStream zis = new ZipInputStream(stream);
355 ZipEntry entry;
356 File appDir = new File(appsDir, desc.name());
357 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800358 if (!entry.isDirectory()) {
359 byte[] data = ByteStreams.toByteArray(zis);
360 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800361 File file = new File(appDir, entry.getName());
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800362 if (isTopLevel(file)) {
363 createParentDirs(file);
364 write(data, file);
365 } else {
366 isSelfContained = true;
367 }
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800368 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800369 }
370 zis.close();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800371 return isSelfContained;
372 }
373
374 // Returns true if the specified file is a top-level app file, i.e. app.xml,
375 // features.xml, .jar or a directory; false if anything else.
376 private boolean isTopLevel(File file) {
377 String name = file.getName();
378 return name.equals(APP_XML) || name.endsWith(FEATURES_XML) || name.endsWith(JAR) || file.isDirectory();
379 }
380
381 // Expands the self-contained JAR stream into the app-specific directory,
382 // using the bundle coordinates retrieved from the features.xml file.
383 private void stageSelfContainedJar(InputStream stream, ApplicationDescription desc)
384 throws IOException {
385 // First extract the bundle coordinates
386 String coords = getSelfContainedBundleCoordinates(desc);
387 if (coords == null) {
388 return;
389 }
390
391 // Split the coordinates into segments and build the file name.
392 String[] f = coords.substring(4).split("/");
393 String base = "m2/" + f[0].replace('.', '/') + "/" + f[1] + "/" + f[2] + "/" + f[1] + "-" + f[2];
394 String jarName = base + (f.length < 4 ? "" : "-" + f[3]) + ".jar";
395 String featuresName = base + "-features.xml";
396
397 // Create the file directory structure and copy the file there.
398 File jar = appFile(desc.name(), jarName);
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700399 boolean ok = jar.getParentFile().exists() || jar.getParentFile().mkdirs();
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800400 if (ok) {
401 Files.write(toByteArray(stream), jar);
402 Files.copy(appFile(desc.name(), FEATURES_XML), appFile(desc.name(), featuresName));
403 if (!appFile(desc.name(), FEATURES_XML).delete()) {
404 log.warn("Unable to delete self-contained application {} features.xml", desc.name());
405 }
406 } else {
407 throw new IOException("Unable to save self-contained application " + desc.name());
408 }
409 }
410
411 // Returns the bundle coordinates from the features.xml file.
412 private String getSelfContainedBundleCoordinates(ApplicationDescription desc) {
413 try {
414 XMLConfiguration cfg = new XMLConfiguration();
415 cfg.setAttributeSplittingDisabled(true);
416 cfg.setDelimiterParsingDisabled(true);
417 cfg.load(appFile(desc.name(), FEATURES_XML));
Thomas Vachuskaad37e372017-08-03 12:07:01 -0700418 return cfg.getString("feature.bundle")
419 .replaceFirst("wrap:", "")
420 .replaceFirst("\\$Bundle-.*$", "");
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800421 } catch (ConfigurationException e) {
422 log.warn("Self-contained application {} has no features.xml", desc.name());
423 return null;
424 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800425 }
426
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800427 // Saves the specified XML stream into app-specific directory.
428 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
429 throws IOException {
430 File file = appFile(desc.name(), APP_XML);
Thomas Vachuskae18a3302015-06-23 12:48:28 -0700431 checkState(!file.getParentFile().exists(), "Application already installed");
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800432 createParentDirs(file);
433 write(stream, file);
434 }
435
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800436 // Saves the specified ZIP stream into a file under app-specific directory.
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800437 private void saveApplication(InputStream stream, ApplicationDescription desc,
438 boolean isSelfContainedJar)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800439 throws IOException {
Thomas Vachuska6cc74882017-01-10 16:01:02 -0800440 String name = desc.name() + (isSelfContainedJar ? JAR : OAR);
441 Files.write(toByteArray(stream), appFile(desc.name(), name));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800442 }
443
444 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800445 private void installArtifacts(ApplicationDescription desc) throws IOException {
446 try {
447 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
448 } catch (NoSuchFileException e) {
449 log.debug("Application {} has no M2 artifacts", desc.name());
450 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800451 }
452
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800453 /**
454 * Marks the app as active by creating token file in the app directory.
455 *
456 * @param appName application name
457 * @return true if file was created
458 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800459 protected boolean setActive(String appName) {
460 try {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800461 File active = appFile(appName, "active");
462 createParentDirs(active);
463 return active.createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800464 } catch (IOException e) {
Thomas Vachuskae965b3d2016-03-03 11:42:48 -0800465 log.warn("Unable to mark app {} as active", appName, e);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800466 throw new ApplicationException("Unable to mark app as active", e);
467 }
468 }
469
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800470 /**
471 * Clears the app as active by deleting token file in the app directory.
472 *
473 * @param appName application name
474 * @return true if file was deleted
475 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800476 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800477 return appFile(appName, "active").delete() && updateTime(appName);
478 }
479
480 /**
481 * Updates the time-stamp of the app descriptor file.
482 *
483 * @param appName application name
484 * @return true if the app descriptor was updated
485 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700486 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800487 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800488 }
489
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800490 /**
491 * Indicates whether the app was marked as active by checking for token file.
492 *
493 * @param appName application name
494 * @return true if the app is marked as active
495 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800496 protected boolean isActive(String appName) {
497 return appFile(appName, "active").exists();
498 }
499
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800500 // Returns the name of the file located under the specified app directory.
501 private File appFile(String appName, String fileName) {
502 return new File(new File(appsDir, appName), fileName);
503 }
504
Jian Li97d6b2d2016-01-20 10:13:43 -0800505 // Returns the icon file located under the specified app directory.
506 private File iconFile(String appName, String fileName) {
507 return new File(new File(appsDir, appName), fileName);
508 }
509
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900510 // Returns the set of Permissions specified in the app.xml file
511 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Thomas Vachuska761f0042015-11-11 19:10:17 -0800512 List<Permission> permissionList = Lists.newArrayList();
Changhoon Yoonb856b812015-08-10 03:47:19 +0900513
514 for (Object o : cfg.getList(APP_PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900515 String name = (String) o;
Changhoon Yoonb856b812015-08-10 03:47:19 +0900516 permissionList.add(new Permission(AppPermission.class.getName(), name));
517 }
518 for (Object o : cfg.getList(NET_PERMISSIONS)) {
519 //TODO: TO BE FLESHED OUT WHEN NETWORK PERMISSIONS ARE SUPPORTED
520 break;
521 }
522
523 List<HierarchicalConfiguration> fields =
524 cfg.configurationsAt(JAVA_PERMISSIONS);
525 for (HierarchicalConfiguration sub : fields) {
526 String classname = sub.getString("classname");
527 String name = sub.getString("name");
528 String actions = sub.getString("actions");
529
530 if (classname != null && name != null) {
531 permissionList.add(new Permission(classname, name, actions));
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900532 }
533 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900534 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900535 }
536
Jian Lic35415d2016-01-14 17:22:31 -0800537 // Returns the byte stream from icon.png file in oar application archive.
538 private byte[] getApplicationIcon(String appName) {
Jian Li97d6b2d2016-01-20 10:13:43 -0800539 File iconFile = iconFile(appName, APP_PNG);
Jian Lic35415d2016-01-14 17:22:31 -0800540 try {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700541 final InputStream iconStream;
542 if (iconFile.exists()) {
543 iconStream = new FileInputStream(iconFile);
544 } else {
545 // assume that we can always fallback to default icon
546 iconStream = ApplicationArchive.class.getResourceAsStream("/" + APP_PNG);
547 }
548 return ByteStreams.toByteArray(iconStream);
Jian Lic35415d2016-01-14 17:22:31 -0800549 } catch (IOException e) {
Brian O'Connora7903ae2016-04-29 15:02:32 -0700550 log.warn("Unable to read app icon for app {}", appName, e);
Jian Lic35415d2016-01-14 17:22:31 -0800551 }
Brian O'Connora7903ae2016-04-29 15:02:32 -0700552 return new byte[0];
Jian Lic35415d2016-01-14 17:22:31 -0800553 }
554
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900555 // Returns application role type
556 public ApplicationRole getRole(String value) {
557 if (value == null) {
558 return ApplicationRole.UNSPECIFIED;
559 } else {
560 try {
561 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
562 } catch (IllegalArgumentException e) {
563 log.debug("Unknown role value: %s", value);
564 return ApplicationRole.UNSPECIFIED;
565 }
566 }
567 }
Jian Lic35415d2016-01-14 17:22:31 -0800568
569 // Returns the first sentence of the given sentence
570 private String compactDescription(String sentence) {
571 if (StringUtils.isNotEmpty(sentence)) {
572 if (StringUtils.contains(sentence, ".")) {
573 return StringUtils.substringBefore(sentence, ".") + ".";
574 } else {
Jian Li8bcb4f22016-01-20 10:36:18 -0800575 return sentence;
Jian Lic35415d2016-01-14 17:22:31 -0800576 }
577 }
578 return sentence;
579 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800580}