blob: f8359fda236c6016e6b84f1850aa1e323b0a3299 [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;
30import org.onosproject.core.Permission;
31import org.onosproject.core.Version;
32import org.onosproject.store.AbstractStore;
33import org.slf4j.Logger;
34import org.slf4j.LoggerFactory;
35
36import java.io.ByteArrayInputStream;
37import java.io.File;
38import java.io.FileInputStream;
39import java.io.FileNotFoundException;
40import java.io.IOException;
41import java.io.InputStream;
42import java.net.URI;
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080043import java.nio.charset.Charset;
Thomas Vachuska90b453f2015-01-30 18:57:14 -080044import java.nio.file.NoSuchFileException;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -080045import java.util.List;
Thomas Vachuska02aeb032015-01-06 22:36:30 -080046import java.util.Set;
47import java.util.zip.ZipEntry;
48import java.util.zip.ZipInputStream;
49
50import static com.google.common.io.ByteStreams.toByteArray;
51import static com.google.common.io.Files.createParentDirs;
52import static com.google.common.io.Files.write;
53
54/**
55 * Facility for reading application archive stream and managing application
56 * directory structure.
57 */
58public class ApplicationArchive
59 extends AbstractStore<ApplicationEvent, ApplicationStoreDelegate> {
60
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070061 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
62
Thomas Vachuska62ad95f2015-02-18 12:11:36 -080063 // Magic strings to search for at the beginning of the archive stream
64 private static final String XML_MAGIC = "<?xml ";
65
66 // Magic strings to search for and how deep to search it into the archive stream
67 private static final String APP_MAGIC = "<app ";
68 private static final int APP_MAGIC_DEPTH = 1024;
69
Thomas Vachuska02aeb032015-01-06 22:36:30 -080070 private static final String NAME = "[@name]";
71 private static final String ORIGIN = "[@origin]";
72 private static final String VERSION = "[@version]";
73 private static final String FEATURES_REPO = "[@featuresRepo]";
74 private static final String FEATURES = "[@features]";
75 private static final String DESCRIPTION = "description";
76
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -070077 private static final String OAR = ".oar";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080078 private static final String APP_XML = "app.xml";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080079 private static final String M2_PREFIX = "m2";
80
Thomas Vachuska40a398b2015-04-03 22:26:30 -070081 private static final String ROOT = "../";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080082 private static final String M2_ROOT = "system/";
Thomas Vachuskad5d9bcb2015-03-18 17:46:20 -070083 private static final String APPS_ROOT = "apps/";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080084
Thomas Vachuska40a398b2015-04-03 22:26:30 -070085 private File root = new File(ROOT);
86 private File appsDir = new File(root, APPS_ROOT);
87 private File m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -080088
89 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -070090 * Sets the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -080091 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -070092 * @param root top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -080093 */
Thomas Vachuska40a398b2015-04-03 22:26:30 -070094 protected void setRootPath(String root) {
95 this.root = new File(root);
96 this.appsDir = new File(this.root, APPS_ROOT);
97 this.m2Dir = new File(M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -080098 }
99
100 /**
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700101 * Returns the root directory where apps directory is contained.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800102 *
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700103 * @return top-level directory path
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800104 */
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800105 protected String getRootPath() {
Thomas Vachuska40a398b2015-04-03 22:26:30 -0700106 return root.getPath();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800107 }
108
109 /**
110 * Returns the set of installed application names.
111 *
112 * @return installed application names
113 */
114 public Set<String> getApplicationNames() {
115 ImmutableSet.Builder<String> names = ImmutableSet.builder();
116 File[] files = appsDir.listFiles(File::isDirectory);
117 if (files != null) {
118 for (File file : files) {
119 names.add(file.getName());
120 }
121 }
122 return names.build();
123 }
124
125 /**
Thomas Vachuskacf960112015-03-06 22:36:51 -0800126 * Returns the timestamp in millis since start of epoch, of when the
127 * specified application was last modified or changed state.
128 *
129 * @param appName application name
130 * @return number of millis since start of epoch
131 */
132 public long getUpdateTime(String appName) {
133 return appFile(appName, APP_XML).lastModified();
134 }
135
136 /**
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800137 * Loads the application descriptor from the specified application archive
138 * stream and saves the stream in the appropriate application archive
139 * directory.
140 *
141 * @param appName application name
142 * @return application descriptor
143 * @throws org.onosproject.app.ApplicationException if unable to read application description
144 */
145 public ApplicationDescription getApplicationDescription(String appName) {
146 try {
147 return loadAppDescription(new XMLConfiguration(appFile(appName, APP_XML)));
148 } catch (Exception e) {
149 throw new ApplicationException("Unable to get app description", e);
150 }
151 }
152
153 /**
154 * Loads the application descriptor from the specified application archive
155 * stream and saves the stream in the appropriate application archive
156 * directory.
157 *
158 * @param stream application archive stream
159 * @return application descriptor
160 * @throws org.onosproject.app.ApplicationException if unable to read the
161 * archive stream or store
162 * the application archive
163 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800164 public synchronized ApplicationDescription saveApplication(InputStream stream) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800165 try (InputStream ais = stream) {
166 byte[] cache = toByteArray(ais);
167 InputStream bis = new ByteArrayInputStream(cache);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800168
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800169 boolean plainXml = isPlainXml(cache);
170 ApplicationDescription desc = plainXml ?
171 parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800172
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800173 if (plainXml) {
174 expandPlainApplication(cache, desc);
175 } else {
176 bis.reset();
177 expandZippedApplication(bis, desc);
178
179 bis.reset();
180 saveApplication(bis, desc);
181 }
182
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800183 installArtifacts(desc);
184 return desc;
185 } catch (IOException e) {
186 throw new ApplicationException("Unable to save application", e);
187 }
188 }
189
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800190 // Indicates whether the stream encoded in the given bytes is plain XML.
191 private boolean isPlainXml(byte[] bytes) {
192 return substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
193 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC);
194 }
195
196 // Returns the substring of maximum possible length from the specified bytes.
197 private String substring(byte[] bytes, int length) {
198 return new String(bytes, 0, Math.min(bytes.length, length), Charset.forName("UTF-8"));
199 }
200
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800201 /**
202 * Purges the application archive directory.
203 *
204 * @param appName application name
205 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800206 public synchronized void purgeApplication(String appName) {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800207 File appDir = new File(appsDir, appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800208 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800209 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800210 } catch (IOException e) {
211 throw new ApplicationException("Unable to purge application " + appName, e);
212 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800213 if (appDir.exists()) {
214 throw new ApplicationException("Unable to purge application " + appName);
215 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800216 }
217
218 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800219 * Returns application archive stream for the specified application. This
220 * will be either the application ZIP file or the application XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800221 *
222 * @param appName application name
223 * @return application archive stream
224 */
Thomas Vachuska0249b532015-02-20 16:46:18 -0800225 public synchronized InputStream getApplicationInputStream(String appName) {
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800226 try {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700227 File appFile = appFile(appName, appName + OAR);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800228 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800229 } catch (FileNotFoundException e) {
230 throw new ApplicationException("Application " + appName + " not found");
231 }
232 }
233
234 // Scans the specified ZIP stream for app.xml entry and parses it producing
235 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800236 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800237 throws IOException {
238 try (ZipInputStream zis = new ZipInputStream(stream)) {
239 ZipEntry entry;
240 while ((entry = zis.getNextEntry()) != null) {
241 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800242 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800243 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800244 }
245 zis.closeEntry();
246 }
247 }
248 throw new IOException("Unable to locate " + APP_XML);
249 }
250
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800251 // Scans the specified XML stream and parses it producing an application descriptor.
252 private ApplicationDescription parsePlainAppDescription(InputStream stream)
253 throws IOException {
254 XMLConfiguration cfg = new XMLConfiguration();
255 try {
256 cfg.load(stream);
257 return loadAppDescription(cfg);
258 } catch (ConfigurationException e) {
259 throw new IOException("Unable to parse " + APP_XML, e);
260 }
261 }
262
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800263 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
264 cfg.setAttributeSplittingDisabled(true);
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800265 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800266 String name = cfg.getString(NAME);
267 Version version = Version.version(cfg.getString(VERSION));
268 String desc = cfg.getString(DESCRIPTION);
269 String origin = cfg.getString(ORIGIN);
270 Set<Permission> perms = ImmutableSet.of();
271 String featRepo = cfg.getString(FEATURES_REPO);
272 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800273 List<String> features = ImmutableList.copyOf(cfg.getStringArray(FEATURES));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800274
275 return new DefaultApplicationDescription(name, version, desc, origin,
276 perms, featuresRepo, features);
277 }
278
279 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800280 private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800281 throws IOException {
282 ZipInputStream zis = new ZipInputStream(stream);
283 ZipEntry entry;
284 File appDir = new File(appsDir, desc.name());
285 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800286 if (!entry.isDirectory()) {
287 byte[] data = ByteStreams.toByteArray(zis);
288 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800289 File file = new File(appDir, entry.getName());
290 createParentDirs(file);
291 write(data, file);
292 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800293 }
294 zis.close();
295 }
296
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800297 // Saves the specified XML stream into app-specific directory.
298 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
299 throws IOException {
300 File file = appFile(desc.name(), APP_XML);
301 createParentDirs(file);
302 write(stream, file);
303 }
304
305
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800306 // Saves the specified ZIP stream into a file under app-specific directory.
307 private void saveApplication(InputStream stream, ApplicationDescription desc)
308 throws IOException {
Thomas Vachuskaa7a0f562015-04-14 23:27:44 -0700309 Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + OAR));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800310 }
311
312 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800313 private void installArtifacts(ApplicationDescription desc) throws IOException {
314 try {
315 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
316 } catch (NoSuchFileException e) {
317 log.debug("Application {} has no M2 artifacts", desc.name());
318 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800319 }
320
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800321 /**
322 * Marks the app as active by creating token file in the app directory.
323 *
324 * @param appName application name
325 * @return true if file was created
326 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800327 protected boolean setActive(String appName) {
328 try {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800329 return appFile(appName, "active").createNewFile() && updateTime(appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800330 } catch (IOException e) {
331 throw new ApplicationException("Unable to mark app as active", e);
332 }
333 }
334
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800335 /**
336 * Clears the app as active by deleting token file in the app directory.
337 *
338 * @param appName application name
339 * @return true if file was deleted
340 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800341 protected boolean clearActive(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800342 return appFile(appName, "active").delete() && updateTime(appName);
343 }
344
345 /**
346 * Updates the time-stamp of the app descriptor file.
347 *
348 * @param appName application name
349 * @return true if the app descriptor was updated
350 */
Thomas Vachuska161baf52015-03-27 16:15:39 -0700351 protected boolean updateTime(String appName) {
Thomas Vachuskacf960112015-03-06 22:36:51 -0800352 return appFile(appName, APP_XML).setLastModified(System.currentTimeMillis());
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800353 }
354
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800355 /**
356 * Indicates whether the app was marked as active by checking for token file.
357 *
358 * @param appName application name
359 * @return true if the app is marked as active
360 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800361 protected boolean isActive(String appName) {
362 return appFile(appName, "active").exists();
363 }
364
365
366 // Returns the name of the file located under the specified app directory.
367 private File appFile(String appName, String fileName) {
368 return new File(new File(appsDir, appName), fileName);
369 }
370
371}