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    }