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