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.jar.JarFile;
031    import java.util.zip.ZipEntry;
032    import java.util.zip.ZipOutputStream;
033    import javax.management.ObjectName;
034    
035    import org.apache.geronimo.gbean.AbstractName;
036    import org.apache.geronimo.gbean.GBeanInfo;
037    import org.apache.geronimo.gbean.GBeanInfoBuilder;
038    import org.apache.geronimo.kernel.Kernel;
039    import org.apache.geronimo.kernel.ObjectNameUtil;
040    import org.apache.geronimo.kernel.config.ConfigurationAlreadyExistsException;
041    import org.apache.geronimo.kernel.config.ConfigurationData;
042    import org.apache.geronimo.kernel.config.ConfigurationInfo;
043    import org.apache.geronimo.kernel.config.ConfigurationStore;
044    import org.apache.geronimo.kernel.config.ConfigurationUtil;
045    import org.apache.geronimo.kernel.config.InvalidConfigException;
046    import org.apache.geronimo.kernel.config.NoSuchConfigException;
047    import org.apache.geronimo.kernel.config.IOUtil;
048    import org.apache.geronimo.kernel.config.PersistentConfigurationList;
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: 487175 $ $Date: 2006-12-14 03:10:31 -0800 (Thu, 14 Dec 2006) $
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 (location.isDirectory()) {
205                    Set matches = IOUtil.search(location, path);
206                    return matches;
207                } else {
208                    Set matches = IOUtil.search(location, path);
209                    return matches;
210                }
211            } else {
212                Set matches = IOUtil.search(location, moduleName + "/" +path);
213                return matches;
214            }
215        }
216    
217        public void exportConfiguration(Artifact configId, OutputStream output) throws IOException, NoSuchConfigException {
218            if(!configId.isResolved()) {
219                throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
220            }
221            File dir = repository.getLocation(configId);
222            if (dir == null) {
223                throw new NoSuchConfigException(configId);
224            }
225            if (existsReadable(dir)) {
226                throw new IOException("Cannot read config store directory for " + configId + " (" + dir.getAbsolutePath() + ")");
227            }
228            ZipOutputStream out = new ZipOutputStream(output);
229            byte[] buf = new byte[10240];
230            writeToZip(dir, out, "", buf);
231            if (inPlaceConfUtil.isInPlaceConfiguration(dir)) {
232                dir = inPlaceConfUtil.readInPlaceLocation(dir);
233                writeToZip(dir, out, "", buf);
234            }
235            out.closeEntry();
236            out.finish();
237            out.flush();
238        }
239    
240        private void writeToZip(File dir, ZipOutputStream out, String prefix, byte[] buf) throws IOException {
241            File[] all = dir.listFiles();
242            for (int i = 0; i < all.length; i++) {
243                File file = all[i];
244                if (file.isDirectory()) {
245                    writeToZip(file, out, prefix + file.getName() + "/", buf);
246                } else {
247                    ZipEntry entry = new ZipEntry(prefix + file.getName());
248                    out.putNextEntry(entry);
249                    writeToZipStream(file, out, buf);
250                }
251            }
252        }
253    
254        private void writeToZipStream(File file, OutputStream out, byte[] buf) throws IOException {
255            FileInputStream in = new FileInputStream(file);
256            int count;
257            try {
258                while ((count = in.read(buf, 0, buf.length)) > -1) {
259                    out.write(buf, 0, count);
260                }
261            } finally {
262                in.close();
263            }
264        }
265    
266        public void install(InputStream in, int size, Artifact configId, FileWriteMonitor fileWriteMonitor) throws IOException {
267            try {
268                repository.copyToRepository(in, size, configId, fileWriteMonitor);
269            } catch (IOException e) {
270                throw e;
271            } finally {
272                IOUtil.close(in);
273            }
274        }
275    
276        public boolean isInPlaceConfiguration(Artifact configId) throws NoSuchConfigException, IOException {
277            if(!configId.isResolved()) {
278                throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
279            }
280            File location = repository.getLocation(configId);
281            if (location.isDirectory()) {
282                return inPlaceConfUtil.isInPlaceConfiguration(location);
283            } else {
284                return false;
285            }
286        }
287    
288        public void install(ConfigurationData configurationData) throws IOException, InvalidConfigException {
289            // determine the source file/dir
290            File source = configurationData.getConfigurationDir();
291            if (!source.exists()) {
292                throw new InvalidConfigException("Source does not exist " + source);
293            } else if (!source.canRead()) {
294                throw new InvalidConfigException("Source is not readable " + source);
295            }
296    
297            // determine the target location
298            Artifact configId = configurationData.getId();
299            File destination = repository.getLocation(configId);
300    
301            // if directory in the correct place -- noop
302            if (!source.equals(destination)) {
303                if (destination.exists()) {
304                    throw new ConfigurationAlreadyExistsException(configId.toString());
305                }
306    
307                if (source.isFile()) {
308                    // Assume this is a jar file
309                    // copy it into the repository; repository should unpack it
310                    repository.copyToRepository(source, configId, null);
311                } else if (source.isDirectory()) {
312                    // directory is in wrong place -- directory copy
313                    IOUtil.recursiveCopy(source, destination);
314                } else {
315                    throw new InvalidConfigException("Unable to install configuration from " + source);
316                }
317            }
318    
319            ExecutableConfigurationUtil.writeConfiguration(configurationData, destination);
320    
321            // write in-place configuration config file, if need be.
322            inPlaceConfUtil.writeInPlaceLocation(configurationData, destination);
323        }
324    
325        public void uninstall(Artifact configId) throws NoSuchConfigException, IOException {
326            if(!configId.isResolved()) {
327                throw new IllegalArgumentException("Artifact "+configId+" is not fully resolved");
328            }
329            ConfigurationInfo configurationInfo = null;
330            try {
331                configurationInfo = loadConfigurationInfo(configId);
332            } catch (IOException e) {
333                // don't really care
334            }
335            File location = repository.getLocation(configId);
336            IOUtil.recursiveDelete(location);
337            // Number of directory levels up, to check and delete empty parent directories in the repo
338            int dirDepth = 0;
339    
340            // FIXME: Determine the repository type
341            // For now assume the repo is a Maven2Repository.  This should not cause any harm even if it is an
342            // Maven1Repository, for it would be deleting the 'repository' directory if it happens to be empty.
343            boolean m2repo = true;
344            if(m2repo) {
345                // Check version, artifact and group directories, i.e. 3 levels up
346                dirDepth = 3;
347            }
348    
349            File temp = location;
350            for(int i = 0; i < dirDepth; ++i) {
351                if((temp = temp.getParentFile()).listFiles().length == 0) {
352                    // Directory is empty.  Remove it.
353                    temp.delete();
354                } else {
355                    // Directory is not empty.  No need to check any more parent directories
356                    break;
357                }
358            }
359    
360            try {
361                // Is this the right way to get hold of PersistentConfigurationList?
362                PersistentConfigurationList configList = (PersistentConfigurationList) kernel.getGBean(PersistentConfigurationList.class);
363                if(!configList.hasGBeanAttributes(configId)) configList.removeConfiguration(configId);
364            } catch (Exception e) {
365                log.warn("Unable to remove configuration from persistent configurations. id = "+configId, e);
366            }
367    
368            if (configurationInfo != null) {
369                IOException ioException = null;
370                for (Iterator iterator = configurationInfo.getOwnedConfigurations().iterator(); iterator.hasNext();) {
371                    Artifact ownedConfiguration = (Artifact) iterator.next();
372                    try {
373                        uninstall(ownedConfiguration);
374                    } catch (NoSuchConfigException e) {
375                        // ignored - already deleted or never installed
376                    } catch (IOException e) {
377                        if (ioException != null) {
378                            ioException = e;
379                        }
380                    }
381                    if (ioException != null) {
382                        throw ioException;
383                    }
384                }
385            }
386        }
387    
388        public List listConfigurations() {
389            SortedSet artifacts = repository.list();
390    
391            List configs;
392            synchronized (this) {
393                configs = new ArrayList();
394                for (Iterator i = artifacts.iterator(); i.hasNext();) {
395                    Artifact configId = (Artifact) i.next();
396                    File dir = repository.getLocation(configId);
397                    File meta = new File(dir, "META-INF");
398                    if(!meta.isDirectory() || !meta.canRead()) {
399                        continue;
400                    }
401                    File ser = new File(meta, "config.ser");
402                    if(!ser.isFile() || !ser.canRead() || ser.length() == 0) {
403                        continue;
404                    }
405                    try {
406                        ConfigurationInfo configurationInfo = loadConfigurationInfo(configId);
407                        configs.add(configurationInfo);
408                    } catch (NoSuchConfigException e) {
409                        log.error("Unexpected error: found META-INF/config.ser for "+configId+" but couldn't load ConfigurationInfo", e);
410                    } catch (IOException e) {
411                        log.error("Unable to load ConfigurationInfo for "+configId, e);
412                    }
413                }
414            }
415            return configs;
416        }
417    
418        private ConfigurationInfo loadConfigurationInfo(Artifact configId) throws NoSuchConfigException, IOException {
419            File location = repository.getLocation(configId);
420    
421            if (!location.exists() && !location.canRead()) {
422                throw new NoSuchConfigException(configId);
423            }
424    
425            File inPlaceLocation = inPlaceConfUtil.readInPlaceLocation(location);
426    
427            ConfigurationInfo configurationInfo;
428            if (location.isDirectory()) {
429                File infoFile = new File(location, "META-INF");
430                infoFile = new File(infoFile, "config.info");
431    
432                InputStream in = new FileInputStream(infoFile);
433                try {
434                    configurationInfo = ConfigurationUtil.readConfigurationInfo(in, getAbstractName(), inPlaceLocation);
435                } finally {
436                    IOUtil.close(in);
437                }
438            } else {
439                JarFile jarFile = new JarFile(location);
440                InputStream in = null;
441                try {
442                    ZipEntry entry = jarFile.getEntry("META-INF/config.info");
443                    in = jarFile.getInputStream(entry);
444                    configurationInfo = ConfigurationUtil.readConfigurationInfo(in, getAbstractName(), inPlaceLocation);
445                } finally {
446                    IOUtil.close(in);
447                    IOUtil.close(jarFile);
448                }
449            }
450    
451            return configurationInfo;
452        }
453    
454    //    /**
455    //     * Thread to cleanup unused Config Store entries.
456    //     * On Windows, open files can't be deleted. Until MultiParentClassLoaders
457    //     * are GC'ed, we won't be able to delete Config Store directories/files.
458    //     */
459    //    class ConfigStoreReaper implements Runnable {
460    //        private final int reaperInterval;
461    //        private volatile boolean done = false;
462    //
463    //        public ConfigStoreReaper(int reaperInterval) {
464    //            this.reaperInterval = reaperInterval;
465    //        }
466    //
467    //        public void close() {
468    //            this.done = true;
469    //        }
470    //
471    //        public void run() {
472    //            log.debug("ConfigStoreReaper started");
473    //            while (!done) {
474    //                try {
475    //                    Thread.sleep(reaperInterval);
476    //                } catch (InterruptedException e) {
477    //                    continue;
478    //                }
479    //                reap();
480    //            }
481    //        }
482    //
483    //        /**
484    //         * For every directory in the pendingDeletionIndex, attempt to delete all
485    //         * sub-directories and files.
486    //         */
487    //        public void reap() {
488    //            // return, if there's nothing to do
489    //            if (pendingDeletionIndex.size() == 0)
490    //                return;
491    //            // Otherwise, attempt to delete all of the directories
492    //            Enumeration list = pendingDeletionIndex.propertyNames();
493    //            boolean dirDeleted = false;
494    //            while (list.hasMoreElements()) {
495    //                String dirName = (String) list.nextElement();
496    //                File deleteFile = new File(dirName);
497    //                try {
498    //                    delete(deleteFile);
499    //                }
500    //                catch (IOException ioe) { // ignore errors
501    //                }
502    //                if (!deleteFile.exists()) {
503    //                    String configName = pendingDeletionIndex.getProperty(dirName);
504    //                    pendingDeletionIndex.remove(dirName);
505    //                    dirDeleted = true;
506    //                    log.debug("Reaped configuration " + configName + " in directory " + dirName);
507    //                }
508    //            }
509    //            // If we deleted any directories, persist the list of directories to disk...
510    //            if (dirDeleted) {
511    //                try {
512    //                    synchronized (pendingDeletionIndex) {
513    //                        saveDeleteIndex();
514    //                    }
515    //                }
516    //                catch (IOException ioe) {
517    //                    log.warn("Error saving " + DELETE_NAME + " file.", ioe);
518    //                }
519    //            }
520    //        }
521    //    }
522    //
523        public static final GBeanInfo GBEAN_INFO;
524    
525        public static GBeanInfo getGBeanInfo() {
526            return GBEAN_INFO;
527        }
528    
529        static {
530            GBeanInfoBuilder builder = GBeanInfoBuilder.createStatic(RepositoryConfigurationStore.class, "ConfigurationStore");
531            builder.addAttribute("kernel", Kernel.class, false);
532            builder.addAttribute("objectName", String.class, false);
533            builder.addReference("Repository", WritableListableRepository.class, "Repository");
534            builder.setConstructor(new String[]{"kernel", "objectName", "Repository"});
535            GBEAN_INFO = builder.getBeanInfo();
536        }
537    }