blob: 5a818e41fc16d3caa0c02c0e946b880d1a10c86a [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 Vachuska62ad95f2015-02-18 12:11:36 -080061 // Magic strings to search for at the beginning of the archive stream
62 private static final String XML_MAGIC = "<?xml ";
63
64 // Magic strings to search for and how deep to search it into the archive stream
65 private static final String APP_MAGIC = "<app ";
66 private static final int APP_MAGIC_DEPTH = 1024;
67
Thomas Vachuska02aeb032015-01-06 22:36:30 -080068 private static final String NAME = "[@name]";
69 private static final String ORIGIN = "[@origin]";
70 private static final String VERSION = "[@version]";
71 private static final String FEATURES_REPO = "[@featuresRepo]";
72 private static final String FEATURES = "[@features]";
73 private static final String DESCRIPTION = "description";
74
75 private static Logger log = LoggerFactory.getLogger(ApplicationArchive.class);
76 private static final String APP_XML = "app.xml";
Thomas Vachuska90b453f2015-01-30 18:57:14 -080077 private static final String M2_PREFIX = "m2";
78
79 private static final String KARAF_ROOT = ".";
80 private static final String M2_ROOT = "system/";
Thomas Vachuska02aeb032015-01-06 22:36:30 -080081 private static final String APPS_ROOT = "data/apps/";
82
Thomas Vachuska90b453f2015-01-30 18:57:14 -080083 private File karafRoot = new File(KARAF_ROOT);
84 private File m2Dir = new File(karafRoot, M2_ROOT);
85 private File appsDir = new File(karafRoot, APPS_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -080086
87 /**
88 * Sets the root directory where application artifacts are kept.
89 *
90 * @param appsRoot top-level applications directory path
91 */
Thomas Vachuska90b453f2015-01-30 18:57:14 -080092 protected void setRootPath(String appsRoot) {
93 this.karafRoot = new File(appsRoot);
94 this.appsDir = new File(karafRoot, APPS_ROOT);
95 this.m2Dir = new File(karafRoot, M2_ROOT);
Thomas Vachuska02aeb032015-01-06 22:36:30 -080096 }
97
98 /**
99 * Returns the root directory where application artifacts are kept.
100 *
101 * @return top-level applications directory path
102 */
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800103 protected String getRootPath() {
104 return karafRoot.getPath();
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800105 }
106
107 /**
108 * Returns the set of installed application names.
109 *
110 * @return installed application names
111 */
112 public Set<String> getApplicationNames() {
113 ImmutableSet.Builder<String> names = ImmutableSet.builder();
114 File[] files = appsDir.listFiles(File::isDirectory);
115 if (files != null) {
116 for (File file : files) {
117 names.add(file.getName());
118 }
119 }
120 return names.build();
121 }
122
123 /**
124 * Loads the application descriptor from the specified application archive
125 * stream and saves the stream in the appropriate application archive
126 * directory.
127 *
128 * @param appName application name
129 * @return application descriptor
130 * @throws org.onosproject.app.ApplicationException if unable to read application description
131 */
132 public ApplicationDescription getApplicationDescription(String appName) {
133 try {
134 return loadAppDescription(new XMLConfiguration(appFile(appName, APP_XML)));
135 } catch (Exception e) {
136 throw new ApplicationException("Unable to get app description", e);
137 }
138 }
139
140 /**
141 * Loads the application descriptor from the specified application archive
142 * stream and saves the stream in the appropriate application archive
143 * directory.
144 *
145 * @param stream application archive stream
146 * @return application descriptor
147 * @throws org.onosproject.app.ApplicationException if unable to read the
148 * archive stream or store
149 * the application archive
150 */
151 public ApplicationDescription saveApplication(InputStream stream) {
152 try (InputStream ais = stream) {
153 byte[] cache = toByteArray(ais);
154 InputStream bis = new ByteArrayInputStream(cache);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800155
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800156 boolean plainXml = isPlainXml(cache);
157 ApplicationDescription desc = plainXml ?
158 parsePlainAppDescription(bis) : parseZippedAppDescription(bis);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800159
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800160 if (plainXml) {
161 expandPlainApplication(cache, desc);
162 } else {
163 bis.reset();
164 expandZippedApplication(bis, desc);
165
166 bis.reset();
167 saveApplication(bis, desc);
168 }
169
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800170 installArtifacts(desc);
171 return desc;
172 } catch (IOException e) {
173 throw new ApplicationException("Unable to save application", e);
174 }
175 }
176
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800177 // Indicates whether the stream encoded in the given bytes is plain XML.
178 private boolean isPlainXml(byte[] bytes) {
179 return substring(bytes, XML_MAGIC.length()).equals(XML_MAGIC) ||
180 substring(bytes, APP_MAGIC_DEPTH).contains(APP_MAGIC);
181 }
182
183 // Returns the substring of maximum possible length from the specified bytes.
184 private String substring(byte[] bytes, int length) {
185 return new String(bytes, 0, Math.min(bytes.length, length), Charset.forName("UTF-8"));
186 }
187
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800188 /**
189 * Purges the application archive directory.
190 *
191 * @param appName application name
192 */
193 public void purgeApplication(String appName) {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800194 File appDir = new File(appsDir, appName);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800195 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800196 Tools.removeDirectory(appDir);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800197 } catch (IOException e) {
198 throw new ApplicationException("Unable to purge application " + appName, e);
199 }
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800200 if (appDir.exists()) {
201 throw new ApplicationException("Unable to purge application " + appName);
202 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800203 }
204
205 /**
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800206 * Returns application archive stream for the specified application. This
207 * will be either the application ZIP file or the application XML file.
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800208 *
209 * @param appName application name
210 * @return application archive stream
211 */
212 public InputStream getApplicationInputStream(String appName) {
213 try {
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800214 File appFile = appFile(appName, appName + ".zip");
215 return new FileInputStream(appFile.exists() ? appFile : appFile(appName, APP_XML));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800216 } catch (FileNotFoundException e) {
217 throw new ApplicationException("Application " + appName + " not found");
218 }
219 }
220
221 // Scans the specified ZIP stream for app.xml entry and parses it producing
222 // an application descriptor.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800223 private ApplicationDescription parseZippedAppDescription(InputStream stream)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800224 throws IOException {
225 try (ZipInputStream zis = new ZipInputStream(stream)) {
226 ZipEntry entry;
227 while ((entry = zis.getNextEntry()) != null) {
228 if (entry.getName().equals(APP_XML)) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800229 byte[] data = ByteStreams.toByteArray(zis);
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800230 return parsePlainAppDescription(new ByteArrayInputStream(data));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800231 }
232 zis.closeEntry();
233 }
234 }
235 throw new IOException("Unable to locate " + APP_XML);
236 }
237
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800238 // Scans the specified XML stream and parses it producing an application descriptor.
239 private ApplicationDescription parsePlainAppDescription(InputStream stream)
240 throws IOException {
241 XMLConfiguration cfg = new XMLConfiguration();
242 try {
243 cfg.load(stream);
244 return loadAppDescription(cfg);
245 } catch (ConfigurationException e) {
246 throw new IOException("Unable to parse " + APP_XML, e);
247 }
248 }
249
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800250 private ApplicationDescription loadAppDescription(XMLConfiguration cfg) {
251 cfg.setAttributeSplittingDisabled(true);
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800252 cfg.setDelimiterParsingDisabled(true);
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800253 String name = cfg.getString(NAME);
254 Version version = Version.version(cfg.getString(VERSION));
255 String desc = cfg.getString(DESCRIPTION);
256 String origin = cfg.getString(ORIGIN);
257 Set<Permission> perms = ImmutableSet.of();
258 String featRepo = cfg.getString(FEATURES_REPO);
259 URI featuresRepo = featRepo != null ? URI.create(featRepo) : null;
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800260 List<String> features = ImmutableList.copyOf(cfg.getStringArray(FEATURES));
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800261
262 return new DefaultApplicationDescription(name, version, desc, origin,
263 perms, featuresRepo, features);
264 }
265
266 // Expands the specified ZIP stream into app-specific directory.
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800267 private void expandZippedApplication(InputStream stream, ApplicationDescription desc)
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800268 throws IOException {
269 ZipInputStream zis = new ZipInputStream(stream);
270 ZipEntry entry;
271 File appDir = new File(appsDir, desc.name());
272 while ((entry = zis.getNextEntry()) != null) {
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800273 if (!entry.isDirectory()) {
274 byte[] data = ByteStreams.toByteArray(zis);
275 zis.closeEntry();
Thomas Vachuskaebf5e542015-02-03 19:38:13 -0800276 File file = new File(appDir, entry.getName());
277 createParentDirs(file);
278 write(data, file);
279 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800280 }
281 zis.close();
282 }
283
Thomas Vachuska62ad95f2015-02-18 12:11:36 -0800284 // Saves the specified XML stream into app-specific directory.
285 private void expandPlainApplication(byte[] stream, ApplicationDescription desc)
286 throws IOException {
287 File file = appFile(desc.name(), APP_XML);
288 createParentDirs(file);
289 write(stream, file);
290 }
291
292
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800293 // Saves the specified ZIP stream into a file under app-specific directory.
294 private void saveApplication(InputStream stream, ApplicationDescription desc)
295 throws IOException {
296 Files.write(toByteArray(stream), appFile(desc.name(), desc.name() + ".zip"));
297 }
298
299 // Installs application artifacts into M2 repository.
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800300 private void installArtifacts(ApplicationDescription desc) throws IOException {
301 try {
302 Tools.copyDirectory(appFile(desc.name(), M2_PREFIX), m2Dir);
303 } catch (NoSuchFileException e) {
304 log.debug("Application {} has no M2 artifacts", desc.name());
305 }
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800306 }
307
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800308 /**
309 * Marks the app as active by creating token file in the app directory.
310 *
311 * @param appName application name
312 * @return true if file was created
313 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800314 protected boolean setActive(String appName) {
315 try {
316 return appFile(appName, "active").createNewFile();
317 } catch (IOException e) {
318 throw new ApplicationException("Unable to mark app as active", e);
319 }
320 }
321
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800322 /**
323 * Clears the app as active by deleting token file in the app directory.
324 *
325 * @param appName application name
326 * @return true if file was deleted
327 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800328 protected boolean clearActive(String appName) {
329 return appFile(appName, "active").delete();
330 }
331
Thomas Vachuska90b453f2015-01-30 18:57:14 -0800332 /**
333 * Indicates whether the app was marked as active by checking for token file.
334 *
335 * @param appName application name
336 * @return true if the app is marked as active
337 */
Thomas Vachuska02aeb032015-01-06 22:36:30 -0800338 protected boolean isActive(String appName) {
339 return appFile(appName, "active").exists();
340 }
341
342
343 // Returns the name of the file located under the specified app directory.
344 private File appFile(String appName, String fileName) {
345 return new File(new File(appsDir, appName), fileName);
346 }
347
348}