001 /**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 * http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017 package org.apache.geronimo.system.configuration;
018
019 import java.io.File;
020 import java.io.FileInputStream;
021 import java.io.IOException;
022 import java.io.InputStream;
023 import java.io.OutputStream;
024 import java.net.MalformedURLException;
025 import java.net.URL;
026 import java.util.ArrayList;
027 import java.util.Iterator;
028 import java.util.List;
029 import java.util.SortedSet;
030 import java.util.Set;
031 import java.util.jar.JarFile;
032 import java.util.zip.ZipEntry;
033 import java.util.zip.ZipOutputStream;
034 import javax.management.ObjectName;
035
036 import org.apache.geronimo.gbean.AbstractName;
037 import org.apache.geronimo.gbean.GBeanInfo;
038 import org.apache.geronimo.gbean.GBeanInfoBuilder;
039 import org.apache.geronimo.kernel.Kernel;
040 import org.apache.geronimo.kernel.ObjectNameUtil;
041 import org.apache.geronimo.kernel.config.ConfigurationAlreadyExistsException;
042 import org.apache.geronimo.kernel.config.ConfigurationData;
043 import org.apache.geronimo.kernel.config.ConfigurationInfo;
044 import org.apache.geronimo.kernel.config.ConfigurationStore;
045 import org.apache.geronimo.kernel.config.ConfigurationUtil;
046 import org.apache.geronimo.kernel.config.InvalidConfigException;
047 import org.apache.geronimo.kernel.config.NoSuchConfigException;
048 import org.apache.geronimo.kernel.config.IOUtil;
049 import org.apache.geronimo.kernel.repository.Artifact;
050 import org.apache.geronimo.kernel.repository.WritableListableRepository;
051 import org.apache.commons.logging.Log;
052 import org.apache.commons.logging.LogFactory;
053
054 /**
055 * Implementation of ConfigurationStore GBean that installs/loads Configurations from a
056 * repository.
057 *
058 * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $
059 */
060 public class RepositoryConfigurationStore implements ConfigurationStore {
061 private final static Log log = LogFactory.getLog(RepositoryConfigurationStore.class);
062 private final Kernel kernel;
063 private final ObjectName objectName;
064 private final AbstractName abstractName;
065 protected final WritableListableRepository repository;
066 private final InPlaceConfigurationUtil inPlaceConfUtil;
067
068 public RepositoryConfigurationStore(WritableListableRepository repository) {
069 this(null, null, null, repository);
070 }
071
072 public RepositoryConfigurationStore(Kernel kernel, String objectName, AbstractName abstractName, WritableListableRepository repository) {
073 this.kernel = kernel;
074 this.objectName = objectName == null ? null : ObjectNameUtil.getObjectName(objectName);
075 this.abstractName = abstractName;
076 this.repository = repository;
077
078 inPlaceConfUtil = new InPlaceConfigurationUtil();
079 }
080
081 public String getObjectName() {
082 return objectName.getCanonicalName();
083 }
084
085 public AbstractName getAbstractName() {
086 return abstractName;
087 }
088
089 public ConfigurationData loadConfiguration(Artifact configId) throws NoSuchConfigException, IOException, InvalidConfigException {
090 if(!configId.isResolved()) {
091 throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
092 }
093 File location = repository.getLocation(configId);
094
095 if (existsReadable(location)) {
096 throw new NoSuchConfigException(configId);
097 }
098
099 ConfigurationData configurationData;
100 try {
101 if (location.isDirectory()) {
102 File serFile = new File(location, "META-INF");
103 serFile = new File(serFile, "config.ser");
104
105 if (!serFile.exists()) {
106 throw new InvalidConfigException("Configuration does not contain a META-INF/config.ser file: " + serFile);
107 } else if (!serFile.canRead()) {
108 throw new InvalidConfigException("Can not read configuration META-INF/config.ser file: " + serFile);
109 }
110
111 ConfigurationStoreUtil.verifyChecksum(serFile);
112
113 InputStream in = new FileInputStream(serFile);
114 try {
115 configurationData = ConfigurationUtil.readConfigurationData(in);
116 } finally {
117 IOUtil.close(in);
118 }
119 } else {
120 JarFile jarFile = new JarFile(location);
121 InputStream in = null;
122 try {
123 ZipEntry entry = jarFile.getEntry("META-INF/config.ser");
124 in = jarFile.getInputStream(entry);
125 configurationData = ConfigurationUtil.readConfigurationData(in);
126 } finally {
127 IOUtil.close(in);
128 IOUtil.close(jarFile);
129 }
130 }
131 } catch (ClassNotFoundException e) {
132 throw new InvalidConfigException("Unable to load class from config: " + configId, e);
133 }
134
135 configurationData.setConfigurationDir(location);
136 configurationData.setConfigurationStore(this);
137 if (kernel != null) {
138 configurationData.setNaming(kernel.getNaming());
139 }
140
141 return configurationData;
142 }
143
144 private boolean existsReadable(File location) {
145 return !location.exists() || !location.canRead();
146 }
147
148 public boolean containsConfiguration(Artifact configId) {
149 if(!configId.isResolved()) {
150 throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
151 }
152 File location = repository.getLocation(configId);
153 if (location.isDirectory()) {
154 location = new File(location, "META-INF");
155 location = new File(location, "config.ser");
156 return location.isFile() && location.canRead();
157 } else {
158 JarFile jarFile = null;
159 try {
160 jarFile = new JarFile(location);
161 ZipEntry entry = jarFile.getEntry("META-INF/config.ser");
162 return entry != null && !entry.isDirectory();
163 } catch (IOException e) {
164 return false;
165 } finally {
166 IOUtil.close(jarFile);
167 }
168 }
169 }
170
171 public File createNewConfigurationDir(Artifact configId) throws ConfigurationAlreadyExistsException {
172 if(!configId.isResolved()) {
173 throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
174 }
175 File location = repository.getLocation(configId);
176 if (location.exists()) {
177 boolean isEmptyDirectory = false;
178 if (location.isDirectory()) {
179 File[] files = location.listFiles();
180 isEmptyDirectory = files.length < 1;
181 if (!isEmptyDirectory && log.isDebugEnabled()) {
182 log.debug(location.getPath() + " has " + files.length + " files:");
183 for (File file : files) {
184 log.debug(file.getPath());
185 }
186 }
187 }
188 if (isEmptyDirectory) {
189 if (log.isDebugEnabled()) {
190 log.debug(location.getPath() + " is empty");
191 }
192 } else {
193 log.error(location.getPath() + " is not an empty directory");
194 throw new ConfigurationAlreadyExistsException("Configuration already exists: " + configId);
195 }
196 } else {
197 if (log.isDebugEnabled()) {
198 log.debug("Creating configuration directory: " + location.getPath());
199 }
200 location.mkdirs();
201 }
202 if (!location.exists()) {
203 throw new ConfigurationAlreadyExistsException("Could not create configuration directory: " + location);
204 }
205 return location;
206 }
207
208 public Set<URL> resolve(Artifact configId, String moduleName, String path) throws NoSuchConfigException, MalformedURLException {
209 if(!configId.isResolved()) {
210 throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
211 }
212 File location = repository.getLocation(configId);
213 if (location.isDirectory()) {
214 File inPlaceLocation = null;
215 try {
216 inPlaceLocation = inPlaceConfUtil.readInPlaceLocation(location);
217 } catch (IOException e) {
218 //ignore
219 }
220 if (null != inPlaceLocation) {
221 location = inPlaceLocation;
222 }
223
224 if (moduleName != null) {
225 location = new File(location, moduleName);
226 }
227 return IOUtil.search(location, path);
228 /* if(path == null) {
229 return Collections.singleton(location.toURL());
230 } else {
231 if (location.isDirectory()) {
232 Set matches = IOUtil.search(location, path);
233 return matches;
234 } else {
235 Set matches = IOUtil.search(location, path);
236 return matches;
237 }
238 }
239 */ } else {
240 if (moduleName != null) {
241 path = moduleName + "/" +path;
242 }
243 return IOUtil.search(location, path);
244 }
245 }
246
247 public void exportConfiguration(Artifact configId, OutputStream output) throws IOException, NoSuchConfigException {
248 if(!configId.isResolved()) {
249 throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
250 }
251 File dir = repository.getLocation(configId);
252 if (dir == null) {
253 throw new NoSuchConfigException(configId);
254 }
255 if (existsReadable(dir)) {
256 throw new IOException("Cannot read config store directory for " + configId + " (" + dir.getAbsolutePath() + ")");
257 }
258 ZipOutputStream out = new ZipOutputStream(output);
259 byte[] buf = new byte[10240];
260 writeToZip(dir, out, "", buf);
261 if (inPlaceConfUtil.isInPlaceConfiguration(dir)) {
262 dir = inPlaceConfUtil.readInPlaceLocation(dir);
263 writeToZip(dir, out, "", buf);
264 }
265 out.closeEntry();
266 out.finish();
267 out.flush();
268 }
269
270 private void writeToZip(File dir, ZipOutputStream out, String prefix, byte[] buf) throws IOException {
271 File[] all = dir.listFiles();
272 for (File file : all) {
273 if (file.isDirectory()) {
274 writeToZip(file, out, prefix + file.getName() + "/", buf);
275 } else {
276 ZipEntry entry = new ZipEntry(prefix + file.getName());
277 out.putNextEntry(entry);
278 writeToZipStream(file, out, buf);
279 }
280 }
281 }
282
283 private void writeToZipStream(File file, OutputStream out, byte[] buf) throws IOException {
284 FileInputStream in = new FileInputStream(file);
285 int count;
286 try {
287 while ((count = in.read(buf, 0, buf.length)) > -1) {
288 out.write(buf, 0, count);
289 }
290 } finally {
291 in.close();
292 }
293 }
294
295 public boolean isInPlaceConfiguration(Artifact configId) throws NoSuchConfigException, IOException {
296 if(!configId.isResolved()) {
297 throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
298 }
299 File location = repository.getLocation(configId);
300 if (location.isDirectory()) {
301 return inPlaceConfUtil.isInPlaceConfiguration(location);
302 } else {
303 return false;
304 }
305 }
306
307 public void install(ConfigurationData configurationData) throws IOException, InvalidConfigException {
308 // determine the source file/dir
309 File source = configurationData.getConfigurationDir();
310 if (!source.exists()) {
311 throw new InvalidConfigException("Source does not exist " + source);
312 } else if (!source.canRead()) {
313 throw new InvalidConfigException("Source is not readable " + source);
314 }
315
316 // determine the target location
317 Artifact configId = configurationData.getId();
318 File destination = repository.getLocation(configId);
319
320 // if directory in the correct place -- noop
321 if (!source.equals(destination)) {
322 if (destination.exists()) {
323 throw new ConfigurationAlreadyExistsException(configId.toString());
324 }
325
326 if (source.isFile()) {
327 // Assume this is a jar file
328 // copy it into the repository; repository should unpack it
329 repository.copyToRepository(source, configId, null);
330 } else if (source.isDirectory()) {
331 // directory is in wrong place -- directory copy
332 IOUtil.recursiveCopy(source, destination);
333 } else {
334 throw new InvalidConfigException("Unable to install configuration from " + source);
335 }
336 }
337
338 ExecutableConfigurationUtil.writeConfiguration(configurationData, destination);
339
340 // write in-place configuration config file, if need be.
341 inPlaceConfUtil.writeInPlaceLocation(configurationData, destination);
342 }
343
344 public void uninstall(Artifact configId) throws NoSuchConfigException, IOException {
345 if(!configId.isResolved()) {
346 throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
347 }
348 ConfigurationInfo configurationInfo = null;
349 try {
350 configurationInfo = loadConfigurationInfo(configId);
351 } catch (IOException e) {
352 // don't really care
353 }
354 File location = repository.getLocation(configId);
355 IOUtil.recursiveDelete(location);
356 // Number of directory levels up, to check and delete empty parent directories in the repo
357 int dirDepth = 0;
358
359 // FIXME: Determine the repository type
360 // For now assume the repo is a Maven2Repository. This should not cause any harm even if it is an
361 // Maven1Repository, for it would be deleting the 'repository' directory if it happens to be empty.
362 boolean m2repo = true;
363 if(m2repo) {
364 // Check version, artifact and group directories, i.e. 3 levels up
365 dirDepth = 3;
366 }
367
368 File temp = location;
369 for(int i = 0; i < dirDepth; ++i) {
370 if((temp = temp.getParentFile()).listFiles().length == 0) {
371 // Directory is empty. Remove it.
372 temp.delete();
373 } else {
374 // Directory is not empty. No need to check any more parent directories
375 break;
376 }
377 }
378
379 if (configurationInfo != null) {
380 IOException ioException = null;
381 for (Iterator iterator = configurationInfo.getOwnedConfigurations().iterator(); iterator.hasNext();) {
382 Artifact ownedConfiguration = (Artifact) iterator.next();
383 try {
384 uninstall(ownedConfiguration);
385 } catch (NoSuchConfigException e) {
386 // ignored - already deleted or never installed
387 } catch (IOException e) {
388 if (ioException != null) {
389 ioException = e;
390 }
391 }
392 }
393 if (ioException != null) {
394 throw ioException;
395 }
396 }
397 }
398
399 public List<ConfigurationInfo> listConfigurations() {
400 SortedSet<Artifact> artifacts = repository.list();
401
402 List<ConfigurationInfo> configs= new ArrayList<ConfigurationInfo>();
403 synchronized (this) {
404 for (Artifact configId : artifacts) {
405 File dir = repository.getLocation(configId);
406 File meta = new File(dir, "META-INF");
407 if (!meta.isDirectory() || !meta.canRead()) {
408 continue;
409 }
410 File ser = new File(meta, "config.ser");
411 if (!ser.isFile() || !ser.canRead() || ser.length() == 0) {
412 continue;
413 }
414 try {
415 ConfigurationInfo configurationInfo = loadConfigurationInfo(configId);
416 configs.add(configurationInfo);
417 } catch (NoSuchConfigException e) {
418 log.error("Unexpected error: found META-INF/config.ser for " + configId + " but couldn't load ConfigurationInfo", e);
419 } catch (IOException e) {
420 log.error("Unable to load ConfigurationInfo for " + configId, e);
421 }
422 }
423 }
424 return configs;
425 }
426
427 private ConfigurationInfo loadConfigurationInfo(Artifact configId) throws NoSuchConfigException, IOException {
428 File location = repository.getLocation(configId);
429
430 if (!location.exists() && !location.canRead()) {
431 throw new NoSuchConfigException(configId);
432 }
433
434 File inPlaceLocation = inPlaceConfUtil.readInPlaceLocation(location);
435
436 ConfigurationInfo configurationInfo;
437 if (location.isDirectory()) {
438 File infoFile = new File(location, "META-INF");
439 infoFile = new File(infoFile, "config.info");
440
441 InputStream in = new FileInputStream(infoFile);
442 try {
443 configurationInfo = ConfigurationUtil.readConfigurationInfo(in, getAbstractName(), inPlaceLocation);
444 } finally {
445 IOUtil.close(in);
446 }
447 } else {
448 JarFile jarFile = new JarFile(location);
449 InputStream in = null;
450 try {
451 ZipEntry entry = jarFile.getEntry("META-INF/config.info");
452 in = jarFile.getInputStream(entry);
453 configurationInfo = ConfigurationUtil.readConfigurationInfo(in, getAbstractName(), inPlaceLocation);
454 } finally {
455 IOUtil.close(in);
456 IOUtil.close(jarFile);
457 }
458 }
459
460 return configurationInfo;
461 }
462
463 // /**
464 // * Thread to cleanup unused Config Store entries.
465 // * On Windows, open files can't be deleted. Until MultiParentClassLoaders
466 // * are GC'ed, we won't be able to delete Config Store directories/files.
467 // */
468 // class ConfigStoreReaper implements Runnable {
469 // private final int reaperInterval;
470 // private volatile boolean done = false;
471 //
472 // public ConfigStoreReaper(int reaperInterval) {
473 // this.reaperInterval = reaperInterval;
474 // }
475 //
476 // public void close() {
477 // this.done = true;
478 // }
479 //
480 // public void run() {
481 // log.debug("ConfigStoreReaper started");
482 // while (!done) {
483 // try {
484 // Thread.sleep(reaperInterval);
485 // } catch (InterruptedException e) {
486 // continue;
487 // }
488 // reap();
489 // }
490 // }
491 //
492 // /**
493 // * For every directory in the pendingDeletionIndex, attempt to delete all
494 // * sub-directories and files.
495 // */
496 // public void reap() {
497 // // return, if there's nothing to do
498 // if (pendingDeletionIndex.size() == 0)
499 // return;
500 // // Otherwise, attempt to delete all of the directories
501 // Enumeration list = pendingDeletionIndex.propertyNames();
502 // boolean dirDeleted = false;
503 // while (list.hasMoreElements()) {
504 // String dirName = (String) list.nextElement();
505 // File deleteFile = new File(dirName);
506 // try {
507 // delete(deleteFile);
508 // }
509 // catch (IOException ioe) { // ignore errors
510 // }
511 // if (!deleteFile.exists()) {
512 // String configName = pendingDeletionIndex.getProperty(dirName);
513 // pendingDeletionIndex.remove(dirName);
514 // dirDeleted = true;
515 // log.debug("Reaped configuration " + configName + " in directory " + dirName);
516 // }
517 // }
518 // // If we deleted any directories, persist the list of directories to disk...
519 // if (dirDeleted) {
520 // try {
521 // synchronized (pendingDeletionIndex) {
522 // saveDeleteIndex();
523 // }
524 // }
525 // catch (IOException ioe) {
526 // log.warn("Error saving " + DELETE_NAME + " file.", ioe);
527 // }
528 // }
529 // }
530 // }
531 //
532 public static final GBeanInfo GBEAN_INFO;
533
534 public static GBeanInfo getGBeanInfo() {
535 return GBEAN_INFO;
536 }
537
538 static {
539 GBeanInfoBuilder builder = GBeanInfoBuilder.createStatic(RepositoryConfigurationStore.class, "ConfigurationStore");
540 builder.addAttribute("kernel", Kernel.class, false);
541 builder.addAttribute("objectName", String.class, false);
542 builder.addAttribute("abstractName", AbstractName.class, false);
543 builder.addReference("Repository", WritableListableRepository.class, "Repository");
544 builder.setConstructor(new String[]{"kernel", "objectName", "abstractName", "Repository"});
545 GBEAN_INFO = builder.getBeanInfo();
546 }
547 }