blob: 78fd0b9407c7b8831c7a93f22a2ad8098e2c83d0 [file] [log] [blame]
Thomas Vachuska02aeb032015-01-06 22:36:30 -08001/*
2 * Copyright 2015 Open Networking Laboratory
3 *
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;
20import com.google.common.io.ByteStreams;
21import com.google.common.io.Files;
22import org.apache.commons.configuration.ConfigurationException;
23import org.apache.commons.configuration.XMLConfiguration;
24import org.onlab.util.Tools;
25import org.onosproject.app.ApplicationDescription;
26import org.onosproject.app.ApplicationEvent;
27import org.onosproject.app.ApplicationException;
28import org.onosproject.app.ApplicationStoreDelegate;
29import org.onosproject.app.DefaultApplicationDescription;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090030import org.onosproject.core.ApplicationRole;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080031import org.onosproject.core.Permission;
32import org.onosproject.core.Version;
33import org.onosproject.store.AbstractStore;
34import org.slf4j.Logger;
35import org.slf4j.LoggerFactory;
36
37import java.io.ByteArrayInputStream;
38import java.io.File;
39import java.io.FileInputStream;
40import java.io.FileNotFoundException;
41import java.io.IOException;
42import java.io.InputStream;
43import java.net.URI;
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080044import java.nio.charset.Charset;
Thomas Vachuska90b453f2015-01-30 18:57:14 -080045import java.nio.file.NoSuchFileException;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090046import java.util.ArrayList;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080047import java.util.List;
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090048import java.util.Locale;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080049import java.util.Set;
50import java.util.zip.ZipEntry;
51import java.util.zip.ZipInputStream;
52
53import static com.google.common.io.ByteStreams.toByteArray;
54import static com.google.common.io.Files.createParentDirs;
55import static com.google.common.io.Files.write;
56
57/**
58 * Facility for reading application archive stream and managing application
59 * directory structure.
60 */
61public class ApplicationArchive
62 extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
63
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070064 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
65
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080066 // Magic strings to search for at the beginning of the archive stream
67 private static final String XML_MAGIC = "<?xml ";
68
69 // Magic strings to search for and how deep to search it into the archive stream
70 private static final String APP_MAGIC = "<app ";
71 private static final int APP_MAGIC_DEPTH = 1024;
72
Thomas Vachuska02aeb032015-01-06 22:36:30 -080073 private static final String NAME = "[@name]";
74 private static final String ORIGIN = "[@origin]";
75 private static final String VERSION = "[@version]";
76 private static final String FEATURES_REPO = "[@featuresRepo]";
77 private static final String FEATURES = "[@features]";
78 private static final String DESCRIPTION = "description";
79
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +090080 private static final String ROLE = "security.role";
81 private static final String PERMISSIONS = "security.permissions.permission";
82
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070083 private static final String OAR = ".oar";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080084 private static final String APP_XML = "app.xml";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080085 private static final String M2_PREFIX = "m2";
86
Thomas Vachuska40a398b2015-04-03 22:26:30 -070087 private static final String ROOT = "../";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080088 private static final String M2_ROOT = "system/";
Thomas Vachuskad5d9bcb2015-03-18 17:46:20 -070089 private static final String APPS_ROOT = "apps/";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080090
Thomas Vachuska40a398b2015-04-03 22:26:30 -070091 private File root = new File(ROOT);
92 private File appsDir = new File(root, APPS_ROOT);
93 private File m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -080094
95 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -070096 * Sets the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -080097 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -070098 * @param root top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -080099 */
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700100 protected void setRootPath(String root) {
101 this.root = new File(root);
102 this.appsDir = new File(this.root, APPS_ROOT);
103 this.m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800104 }
105
106 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700107 * Returns the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800108 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700109 * @return top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800110 */
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800111 protected String getRootPath() {
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700112 return root.getPath();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800113 }
114
115 /**
116 * Returns the set of installed application names.
117 *
118 * @return installed application names
119 */
120 public Set<String> getApplicationNames() {
121 ImmutableSet.Builder<String> names = ImmutableSet.builder();
122 File[] files = appsDir.listFiles(File::isDirectory);
123 if (files != null) {
124 for (File file : files) {
125 names.add(file.getName());
126 }
127 }
128 return names.build();
129 }
130
131 /**
Thomas Vachuskacf960112015-03-06 22:36:51 -0800132 * Returns the timestamp in millis since start of epoch, of when the
133 * specified application was last modified or changed state.
134 *
135 * @param appName application name
136 * @return number of millis since start of epoch
137 */
138 public long getUpdateTime(String appName) {
139 return appFile(appName, APP_XML).lastModified();
140 }
141
142 /**
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800143 * Loads the application descriptor from the specified application archive
144 * stream and saves the stream in the appropriate application archive
145 * directory.
146 *
147 * @param appName application name
148 * @return application descriptor
149 * @throws org.onosproject.app.ApplicationException if unable to read application description
150 */
151 public ApplicationDescription getApplicationDescription(String appName) {
152 try {
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700153 XMLConfiguration cfg = new XMLConfiguration();
154 cfg.setAttributeSplittingDisabled(true);
155 cfg.setDelimiterParsingDisabled(true);
156 cfg.load(appFile(appName, APP_XML));
157 return loadAppDescription(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800158 } catch (Exception e) {
159 throw new ApplicationException("Unable to get app description", e);
160 }
161 }
162
163 /**
164 * Loads the application descriptor from the specified application archive
165 * stream and saves the stream in the appropriate application archive
166 * directory.
167 *
168 * @param stream application archive stream
169 * @return application descriptor
170 * @throws org.onosproject.app.ApplicationException if unable to read the
171 * archive stream or store
172 * the application archive
173 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800174 public synchronized ApplicationDescription saveApplication(InputStream stream) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800175 try (InputStream ais = stream) {
176 byte[] cache = toByteArray(ais);
177 InputStream bis = new ByteArrayInputStream(cache);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800178
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800179 boolean plainXml = isPlainXml(cache);
180 ApplicationDescription desc = plainXml ?
181 parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800182
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800183 if (plainXml) {
184 expandPlainApplication(cache, desc);
185 } else {
186 bis.reset();
187 expandZippedApplication(bis, desc);
188
189 bis.reset();
190 saveApplication(bis, desc);
191 }
192
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800193 installArtifacts(desc);
194 return desc;
195 } catch (IOException e) {
196 throw new ApplicationException("Unable to save application", e);
197 }
198 }
199
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800200 // Indicates whether the stream encoded in the given bytes is plain XML.
201 private boolean isPlainXml(byte[] bytes) {
202 return substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
203 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC);
204 }
205
206 // Returns the substring of maximum possible length from the specified bytes.
207 private String substring(byte[] bytes, int length) {
208 return new String(bytes, 0, Math.min(bytes.length, length), Charset.forName("UTF-8"));
209 }
210
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800211 /**
212 * Purges the application archive directory.
213 *
214 * @param appName application name
215 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800216 public synchronized void purgeApplication(String appName) {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800217 File appDir = new File(appsDir, appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800218 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800219 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800220 } catch (IOException e) {
221 throw new ApplicationException("Unable to purge application " + appName, e);
222 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800223 if (appDir.exists()) {
224 throw new ApplicationException("Unable to purge application " + appName);
225 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800226 }
227
228 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800229 * Returns application archive stream for the specified application. This
230 * will be either the application ZIP file or the application XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800231 *
232 * @param appName application name
233 * @return application archive stream
234 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800235 public synchronized InputStream getApplicationInputStream(String appName) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800236 try {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700237 File appFile = appFile(appName, appName + OAR);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800238 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800239 } catch (FileNotFoundException e) {
240 throw new ApplicationException("Application " + appName + " not found");
241 }
242 }
243
244 // Scans the specified ZIP stream for app.xml entry and parses it producing
245 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800246 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800247 throws IOException {
248 try (ZipInputStream zis = new ZipInputStream(stream)) {
249 ZipEntry entry;
250 while ((entry = zis.getNextEntry()) != null) {
251 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800252 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800253 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800254 }
255 zis.closeEntry();
256 }
257 }
258 throw new IOException("Unable to locate " + APP_XML);
259 }
260
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800261 // Scans the specified XML stream and parses it producing an application descriptor.
262 private ApplicationDescription parsePlainAppDescription(InputStream stream)
263 throws IOException {
264 XMLConfiguration cfg = new XMLConfiguration();
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700265 cfg.setAttributeSplittingDisabled(true);
266 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800267 try {
268 cfg.load(stream);
269 return loadAppDescription(cfg);
270 } catch (ConfigurationException e) {
271 throw new IOException("Unable to parse " + APP_XML, e);
272 }
273 }
274
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800275 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800276 String name = cfg.getString(NAME);
277 Version version = Version.version(cfg.getString(VERSION));
278 String desc = cfg.getString(DESCRIPTION);
279 String origin = cfg.getString(ORIGIN);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900280 ApplicationRole role = getRole(cfg.getString(ROLE));
281 Set<Permission> perms = getPermissions(cfg);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800282 String featRepo = cfg.getString(FEATURES_REPO);
283 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaad35c342015-06-11 17:25:36 -0700284 List<String> features = ImmutableList.copyOf(cfg.getString(FEATURES).split(","));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800285
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900286 return new DefaultApplicationDescription(name, version, desc, origin, role,
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800287 perms, featuresRepo, features);
288 }
289
290 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800291 private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800292 throws IOException {
293 ZipInputStream zis = new ZipInputStream(stream);
294 ZipEntry entry;
295 File appDir = new File(appsDir, desc.name());
296 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800297 if (!entry.isDirectory()) {
298 byte[] data = ByteStreams.toByteArray(zis);
299 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800300 File file = new File(appDir, entry.getName());
301 createParentDirs(file);
302 write(data, file);
303 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800304 }
305 zis.close();
306 }
307
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800308 // Saves the specified XML stream into app-specific directory.
309 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
310 throws IOException {
311 File file = appFile(desc.name(), APP_XML);
312 createParentDirs(file);
313 write(stream, file);
314 }
315
316
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800317 // Saves the specified ZIP stream into a file under app-specific directory.
318 private void saveApplication(InputStream stream, ApplicationDescription desc)
319 throws IOException {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700320 Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + OAR));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800321 }
322
323 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800324 private void installArtifacts(ApplicationDescription desc) throws IOException {
325 try {
326 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
327 } catch (NoSuchFileException e) {
328 log.debug("Application {} has no M2 artifacts", desc.name());
329 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800330 }
331
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800332 /**
333 * Marks the app as active by creating token file in the app directory.
334 *
335 * @param appName application name
336 * @return true if file was created
337 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800338 protected boolean setActive(String appName) {
339 try {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800340 return appFile(appName, "active").createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800341 } catch (IOException e) {
342 throw new ApplicationException("Unable to mark app as active", e);
343 }
344 }
345
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800346 /**
347 * Clears the app as active by deleting token file in the app directory.
348 *
349 * @param appName application name
350 * @return true if file was deleted
351 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800352 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800353 return appFile(appName, "active").delete() && updateTime(appName);
354 }
355
356 /**
357 * Updates the time-stamp of the app descriptor file.
358 *
359 * @param appName application name
360 * @return true if the app descriptor was updated
361 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700362 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800363 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800364 }
365
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800366 /**
367 * Indicates whether the app was marked as active by checking for token file.
368 *
369 * @param appName application name
370 * @return true if the app is marked as active
371 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800372 protected boolean isActive(String appName) {
373 return appFile(appName, "active").exists();
374 }
375
376
377 // Returns the name of the file located under the specified app directory.
378 private File appFile(String appName, String fileName) {
379 return new File(new File(appsDir, appName), fileName);
380 }
381
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900382 // Returns the set of Permissions specified in the app.xml file
383 private ImmutableSet<Permission> getPermissions(XMLConfiguration cfg) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900384 List<Permission> permissionList = new ArrayList();
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900385 for (Object o : cfg.getList(PERMISSIONS)) {
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900386 String name = (String) o;
387 try {
388 Permission perm = Permission.valueOf(name);
389 permissionList.add(perm);
390 } catch (IllegalArgumentException e) {
391 log.debug("Unknown permission specified: %s", name);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900392 }
393 }
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900394 return ImmutableSet.copyOf(permissionList);
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900395 }
396
Changhoon Yoona7841ed2015-05-15 02:51:08 +0900397 //
Changhoon Yoonbdeb88a2015-05-12 20:35:31 +0900398 // Returns application role type
399 public ApplicationRole getRole(String value) {
400 if (value == null) {
401 return ApplicationRole.UNSPECIFIED;
402 } else {
403 try {
404 return ApplicationRole.valueOf(value.toUpperCase(Locale.ENGLISH));
405 } catch (IllegalArgumentException e) {
406 log.debug("Unknown role value: %s", value);
407 return ApplicationRole.UNSPECIFIED;
408 }
409 }
410 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800411}