001    
002    /*
003     * Copyright (C) 2012 Archie L. Cobbs. All rights reserved.
004     *
005     * $Id: SpringPersistentObjectSchemaUpdater.java 269 2012-02-01 23:20:34Z archie.cobbs $
006     */
007    
008    package org.dellroad.stuff.pobj;
009    
010    import java.io.BufferedInputStream;
011    import java.io.IOException;
012    import java.io.InputStream;
013    import java.util.Collection;
014    import java.util.Comparator;
015    
016    import javax.xml.transform.stream.StreamSource;
017    
018    import org.dellroad.stuff.schema.SchemaUpdate;
019    import org.dellroad.stuff.spring.BeanNameComparator;
020    import org.springframework.beans.factory.BeanFactory;
021    import org.springframework.beans.factory.BeanFactoryAware;
022    import org.springframework.beans.factory.DisposableBean;
023    import org.springframework.beans.factory.InitializingBean;
024    import org.springframework.beans.factory.ListableBeanFactory;
025    import org.springframework.core.io.Resource;
026    
027    /**
028     * {@link PersistentObjectSchemaUpdater} optimized for use with Spring:
029     * <ul>
030     * <li>{@link #getOrderingTieBreaker} is overridden to break ties by ordering updates in the same order
031     *  as they are defined in the bean factory.</li>
032     * <li>This class implements {@link InitializingBean} and verifies all required properties are set.</li>
033     * <li>If no updates are {@linkplain #setUpdates explicitly configured}, then all {@link SpringPersistentObjectSchemaUpdate}s
034     *  found in the containing bean factory are automatically configured; this requires that all of the schema updates
035     *  are defined in the same {@link ListableBeanFactory}.</li>
036     * </ul>
037     *
038     * <p>
039     * An example of how this class can be combined with custom XML to define an updater and all its updates:
040     * <blockquote><pre>
041     *  &lt;beans xmlns="http://www.springframework.org/schema/beans"
042     *    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
043     *    xmlns:p="http://www.springframework.org/schema/p"
044     *    xsi:schemaLocation="
045     *      http://www.springframework.org/schema/beans
046     *        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"&gt;
047     *
048     *      &lt;!-- Persistent object delegate; you supply the XML (un)marshaller --&gt;
049     *      <b>&lt;bean id="delegate" class="org.dellroad.stuff.pobj.SpringDelegate"
050     *          p:marshaller-ref="marshaller" p:unmarshaller-ref="unmarshaller"/&gt;</b>
051     *
052     *      &lt;!-- Persistent object schema updater --&gt;
053     *      <b>&lt;bean id="schemaUpdater" class="org.dellroad.stuff.pobj.SpringPersistentObjectSchemaUpdater"
054     *          p:file="/var/example/pobj.xml" p:delegate-ref="delegate"
055     *          p:initialXML="classpath:com/example/initial-pobj.xml"/&gt;</b>
056     *
057     *      &lt;!-- Persistent object bean --&gt;
058     *      <b>&lt;bean scope="prototype" factory-bean="schemaUpdater" factory-method="getPersistentObject"/&gt;</b>
059     *
060     *      &lt;!-- Define a common location for our schema update XSLTs --&gt;
061     *      <b>&lt;bean class="org.dellroad.stuff.pobj.SpringXSLUpdateTransformConfigurer"
062     *          p:prefix="classpath:updates/" p:suffix=".xsl"/&gt;</b>
063     *
064     *      &lt;!-- Schema update #1 --&gt;
065     *      <b>&lt;bean id="update1" class="org.dellroad.stuff.pobj.SpringXSLPersistentObjectSchemaUpdate"
066     *        transform="file:///usr/share/updates/SomeUpdate.xsl"/&gt;</b>
067     *
068     *      &lt;!-- Schema update #2: uses "classpath:updates/update2.xsl" thanks to TransformConfigurer --&gt;
069     *      <b>&lt;bean id="update2" class="org.dellroad.stuff.pobj.SpringXSLPersistentObjectSchemaUpdate"/&gt;</b>
070     *
071     *      &lt;!-- Add more schema updates over time as needed and everything just works... --&gt;
072     *
073     *  &lt;/beans&gt;
074     * </pre></blockquote>
075     *
076     * <p>
077     * The {@link PersistentObject} itself, fully updated, is accessible via {@link #getPersistentObject}.
078     * or, in the above example, via the <code>persistentObject</code> bean.
079     *
080     * @param <T> type of the root persistent object
081     */
082    public class SpringPersistentObjectSchemaUpdater<T> extends PersistentObjectSchemaUpdater<T>
083      implements BeanFactoryAware, InitializingBean, DisposableBean {
084    
085        private ListableBeanFactory beanFactory;
086        private T initialValue;
087        private Resource initialXML;
088    
089        /**
090         * Set the initial value to be used on an uninitialized persistent object.
091         *
092         * <p>
093         * Either this or a {@linkplain #setInitialXML initial XML resource} should be configured
094         * to handle the case that no persistent file exists yet.
095         */
096        public void setInitialValue(T initialValue) {
097            this.initialValue = initialValue;
098        }
099    
100        /**
101         * Set the resource containing the initial value, encoded as XML, to be used on an uninitialized persistent object.
102         * This can be used as an alternative to {@link #setInitialValue}.
103         *
104         * <p>
105         * Either this or an explicit {@linkplain #setInitialValue initial value} should be configured
106         * to handle the case that no persistent file exists yet.
107         */
108        public void setInitialXML(Resource resource) {
109            this.initialXML = resource;
110        }
111    
112        /**
113         * Get the initial value for the persistent object when no persistent file is found.
114         *
115         * <p>
116         * The implementation in {@link SpringPersistentObjectSchemaUpdater} returns the {@linkplain #setInitialValue initial value},
117         * if any, otherwise it falls back to decoding the initial value from the {@linkplain #setInitialXML initial value
118         * resource}, if any. If neither property is configured, null is returned.
119         */
120        @Override
121        protected T getInitialValue() {
122    
123            // If value is provided explicitly, just return it
124            if (this.initialValue != null) {
125                this.log.info("loading initial content from explicitly configured value");
126                return this.initialValue;
127            }
128    
129            // Use configured XML if available
130            if (this.initialXML == null)
131                return null;
132            try {
133                this.log.info("loading initial content from " + this.initialXML.getURI());
134                InputStream input = this.initialXML.getInputStream();
135                try {
136                    return this.delegate.deserialize(
137                      new StreamSource(new BufferedInputStream(input), this.initialXML.getURI().toString()));
138                } finally {
139                    try {
140                        input.close();
141                    } catch (IOException e) {
142                        // ignore
143                    }
144                }
145            } catch (RuntimeException e) {
146                throw e;
147            } catch (Exception e) {
148                throw new PersistentObjectException(e);
149            }
150        }
151    
152        @Override
153        public void setBeanFactory(BeanFactory beanFactory) {
154            if (beanFactory instanceof ListableBeanFactory)
155                this.beanFactory = (ListableBeanFactory)beanFactory;
156        }
157    
158        @Override
159        @SuppressWarnings("unchecked")
160        public void afterPropertiesSet() throws Exception {
161    
162            // Check config
163            if (this.file == null)
164                throw new IllegalArgumentException("no file configured");
165            if (this.writeDelay < 0)
166                throw new IllegalArgumentException("negative writeDelay file configured");
167            if (this.delegate == null)
168                throw new IllegalArgumentException("no delegate configured");
169            if (this.getUpdates() == null) {
170                if (this.beanFactory == null) {
171                    throw new IllegalArgumentException("no updates explicitly configured and the containing BeanFactory"
172                      + " is not a ListableBeanFactory: " + this.beanFactory);
173                }
174                this.setUpdates((Collection<SpringPersistentObjectSchemaUpdate<T>>)(Object)this.beanFactory.getBeansOfType(
175                  SpringPersistentObjectSchemaUpdate.class).values());
176            }
177    
178            // Start
179            this.start();
180        }
181    
182        @Override
183        public void destroy() {
184            this.stop();
185        }
186    
187        /**
188         * Get the preferred ordering of two updates that do not have any predecessor constraints
189         * (including implied indirect constraints) between them.
190         *
191         * <p>
192         * In the case no schema updates are explicitly configured, the {@link Comparator} returned by the
193         * implementation in {@link SpringPersistentObjectSchemaUpdater} sorts updates in the same order that they appear
194         * in the containing {@link ListableBeanFactory}. Otherwise, the
195         * {@linkplain org.dellroad.stuff.schema.AbstractSchemaUpdater#getOrderingTieBreaker superclass method} is used.
196         */
197        @Override
198        protected Comparator<SchemaUpdate<PersistentFileTransaction>> getOrderingTieBreaker() {
199            if (this.beanFactory == null)
200                return super.getOrderingTieBreaker();
201            final BeanNameComparator beanNameComparator = new BeanNameComparator(this.beanFactory);
202            return new Comparator<SchemaUpdate<PersistentFileTransaction>>() {
203                @Override
204                public int compare(SchemaUpdate<PersistentFileTransaction> update1, SchemaUpdate<PersistentFileTransaction> update2) {
205                    return beanNameComparator.compare(update1.getName(), update2.getName());
206                }
207            };
208        }
209    }
210