001    /**
002     * Copyright 2010-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.common.util.service;
017    
018    import java.io.File;
019    import java.util.ArrayList;
020    import java.util.Arrays;
021    import java.util.Collections;
022    import java.util.List;
023    import java.util.Map;
024    
025    import org.kuali.common.util.CollectionUtils;
026    import org.kuali.common.util.LocationUtils;
027    import org.slf4j.Logger;
028    import org.slf4j.LoggerFactory;
029    import org.springframework.beans.factory.BeanFactoryUtils;
030    import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
031    import org.springframework.context.ConfigurableApplicationContext;
032    import org.springframework.context.annotation.AnnotationConfigApplicationContext;
033    import org.springframework.context.support.ClassPathXmlApplicationContext;
034    import org.springframework.context.support.GenericXmlApplicationContext;
035    import org.springframework.core.env.ConfigurableEnvironment;
036    import org.springframework.core.env.MutablePropertySources;
037    import org.springframework.core.env.PropertySource;
038    import org.springframework.util.Assert;
039    
040    public class DefaultSpringService implements SpringService {
041    
042            private static final Logger logger = LoggerFactory.getLogger(DefaultSpringService.class);
043    
044            @Override
045            public List<PropertySource<?>> getPropertySources(String location) {
046                    // Load the indicated location
047                    ConfigurableApplicationContext context = new GenericXmlApplicationContext(location);
048    
049                    // Extract PropertySources (if any)
050                    List<PropertySource<?>> sources = getPropertySources(context);
051    
052                    // Close the context
053                    closeQuietly(context);
054    
055                    // Return the list
056                    return sources;
057            }
058    
059            @Override
060            public List<PropertySource<?>> getPropertySources(ConfigurableApplicationContext context) {
061    
062                    // Extract all beans that implement the PropertySource interface
063                    @SuppressWarnings("rawtypes")
064                    Map<String, PropertySource> map = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, PropertySource.class);
065    
066                    // Convert the Map to a List
067                    List<PropertySource<?>> list = new ArrayList<PropertySource<?>>();
068                    for (PropertySource<?> source : map.values()) {
069                            list.add(source);
070                    }
071    
072                    // Return the list
073                    return list;
074            }
075    
076            @Override
077            public void load(Class<?> annotatedClass) {
078                    load(annotatedClass, null, null);
079            }
080    
081            @Override
082            public void load(Class<?> annotatedClass, String beanName, Object bean, PropertySource<?> propertySource) {
083                    // Make sure the location isn't empty
084                    Assert.notNull(annotatedClass);
085    
086                    List<Class<?>> annotatedClasses = new ArrayList<Class<?>>();
087                    annotatedClasses.add(annotatedClass);
088    
089                    List<PropertySource<?>> propertySources = new ArrayList<PropertySource<?>>();
090                    if (propertySource != null) {
091                            propertySources.add(propertySource);
092                    }
093    
094                    // Setup a SpringContext
095                    SpringContext context = new SpringContext();
096                    context.setAnnotatedClasses(annotatedClasses);
097                    context.setPropertySources(propertySources);
098    
099                    // Null safe handling for non-required parameters
100                    context.setBeanNames(CollectionUtils.toEmptyList(beanName));
101                    context.setBeans(CollectionUtils.toEmptyList(bean));
102    
103                    // Load the configuration from the annotated class
104                    load(context);
105            }
106    
107            @Override
108            public void load(Class<?> annotatedClass, String beanName, Object bean) {
109                    load(annotatedClass, beanName, bean, null);
110            }
111    
112            @Override
113            public void load(String location) {
114                    load(location, null, null);
115            }
116    
117            @Override
118            public void load(String location, String beanName, Object bean, PropertySource<?> propertySource) {
119                    // Make sure the location isn't empty
120                    Assert.hasText(location);
121    
122                    List<PropertySource<?>> propertySources = new ArrayList<PropertySource<?>>();
123                    if (propertySource != null) {
124                            propertySources.add(propertySource);
125                    }
126    
127                    // Setup a SpringContext
128                    SpringContext context = new SpringContext();
129                    context.setLocations(Arrays.asList(location));
130                    context.setPropertySources(propertySources);
131    
132                    // Null safe handling for non-required parameters
133                    context.setBeanNames(CollectionUtils.toEmptyList(beanName));
134                    context.setBeans(CollectionUtils.toEmptyList(bean));
135    
136                    // Load the location using a SpringContext
137                    load(context);
138            }
139    
140            @Override
141            public void load(String location, String beanName, Object bean) {
142                    load(location, beanName, bean, null);
143            }
144    
145            @Override
146            public void load(SpringContext context) {
147    
148                    // Null-safe handling for parameters
149                    context.setBeanNames(CollectionUtils.toEmptyList(context.getBeanNames()));
150                    context.setBeans(CollectionUtils.toEmptyList(context.getBeans()));
151                    context.setAnnotatedClasses(CollectionUtils.toEmptyList(context.getAnnotatedClasses()));
152                    context.setLocations(CollectionUtils.toEmptyList(context.getLocations()));
153    
154                    // Make sure we have at least one location or annotated class
155                    boolean notEmpty = !CollectionUtils.isEmpty(context.getLocations()) || !CollectionUtils.isEmpty(context.getAnnotatedClasses());
156                    Assert.isTrue(notEmpty, "Both locations and annotatedClasses are empty");
157    
158                    // Make sure we have a name for every bean
159                    Assert.isTrue(context.getBeanNames().size() == context.getBeans().size());
160    
161                    // Make sure all of the locations exist
162                    validate(context.getLocations());
163    
164                    // Convert any file names to fully qualified file system URL's
165                    List<String> convertedLocations = getConvertedLocations(context.getLocations());
166    
167                    // The Spring classes prefer array's
168                    String[] locationsArray = CollectionUtils.toStringArray(convertedLocations);
169    
170                    ConfigurableApplicationContext parent = null;
171                    ConfigurableApplicationContext xmlChild = null;
172                    AnnotationConfigApplicationContext annotationChild = null;
173                    try {
174                            if (isParentContextRequired(context)) {
175                                    // Construct a parent context if necessary
176                                    parent = getContextWithPreRegisteredBeans(context.getBeanNames(), context.getBeans());
177                            }
178    
179                            if (!CollectionUtils.isEmpty(context.getAnnotatedClasses())) {
180                                    // Create an annotation based application context wrapped in a parent context
181                                    annotationChild = getAnnotationContext(context, parent);
182                                    // Add custom property sources (if any)
183                                    addPropertySources(context, annotationChild);
184    
185                            }
186    
187                            if (!CollectionUtils.isEmpty(context.getLocations())) {
188                                    // Create an XML application context wrapped in a parent context
189                                    xmlChild = new ClassPathXmlApplicationContext(locationsArray, false, parent);
190                                    // Add custom property sources (if any)
191                                    addPropertySources(context, xmlChild);
192                            }
193    
194                            // Invoke refresh to load the context
195                            refreshQuietly(annotationChild);
196                            refreshQuietly(xmlChild);
197                    } finally {
198                            // cleanup
199                            closeQuietly(annotationChild);
200                            closeQuietly(xmlChild);
201                            closeQuietly(parent);
202                    }
203            }
204    
205            protected AnnotationConfigApplicationContext getAnnotationContext(SpringContext context, ConfigurableApplicationContext parent) {
206                    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
207                    if (parent != null) {
208                            ctx.setParent(parent);
209                    }
210                    for (Class<?> annotatedClass : context.getAnnotatedClasses()) {
211                            ctx.register(annotatedClass);
212                    }
213                    return ctx;
214            }
215    
216            protected void refreshQuietly(ConfigurableApplicationContext context) {
217                    if (context != null) {
218                            context.refresh();
219                    }
220            }
221    
222            /**
223             * Null safe close for a context
224             */
225            protected void closeQuietly(ConfigurableApplicationContext context) {
226                    if (context != null) {
227                            context.close();
228                    }
229            }
230    
231            /**
232             * Return an <code>AbstractApplicationContext</code> with <code>beans</code> and <code>PropertySource's</code> registered as dictated by the <code>SpringContext</code>
233             */
234            @Override
235            public ConfigurableApplicationContext getContextWithPreRegisteredBeans(List<String> beanNames, List<Object> beans) {
236                    Assert.isTrue(beanNames.size() == beans.size());
237                    GenericXmlApplicationContext appContext = new GenericXmlApplicationContext();
238                    appContext.refresh();
239                    ConfigurableListableBeanFactory factory = appContext.getBeanFactory();
240                    for (int i = 0; i < beanNames.size(); i++) {
241                            String beanName = beanNames.get(i);
242                            Object bean = beans.get(i);
243                            logger.debug("Registering bean - [{}] -> [{}]", beanName, bean.getClass().getName());
244                            factory.registerSingleton(beanName, bean);
245                    }
246                    return appContext;
247            }
248    
249            @Override
250            public ConfigurableApplicationContext getContextWithPreRegisteredBean(String beanName, Object bean) {
251                    return getContextWithPreRegisteredBeans(Arrays.asList(beanName), Arrays.asList(bean));
252            }
253    
254            protected void addPropertySources(SpringContext context, ConfigurableApplicationContext applicationContext) {
255                    if (CollectionUtils.isEmpty(context.getPropertySources())) {
256                            return;
257                    }
258                    List<PropertySource<?>> propertySources = context.getPropertySources();
259                    ConfigurableEnvironment environment = applicationContext.getEnvironment();
260                    MutablePropertySources sources = environment.getPropertySources();
261                    if (context.isLastOneInWins()) {
262                            Collections.reverse(propertySources);
263                    }
264                    for (PropertySource<?> propertySource : propertySources) {
265                            logger.debug("Adding property source - [{}] -> [{}]", propertySource.getName(), propertySource.getClass().getName());
266                            sources.addLast(propertySource);
267                    }
268            }
269    
270            /**
271             * Return true if the context contains any beans or beanNames, false otherwise.
272             */
273            protected boolean isParentContextRequired(SpringContext context) {
274                    if (!CollectionUtils.isEmpty(context.getBeanNames())) {
275                            return true;
276                    } else if (!CollectionUtils.isEmpty(context.getBeans())) {
277                            return true;
278                    } else {
279                            return false;
280                    }
281            }
282    
283            /**
284             * Make sure all of the locations actually exist
285             */
286            protected void validate(List<String> locations) {
287                    StringBuilder sb = new StringBuilder();
288                    for (String location : locations) {
289                            if (!LocationUtils.exists(location)) {
290                                    sb.append("Location [" + location + "] does not exist\n");
291                            }
292                    }
293                    if (sb.length() > 0) {
294                            throw new IllegalArgumentException(sb.toString());
295                    }
296            }
297    
298            /**
299             * Format file names into fully qualified file system URL's
300             */
301            protected List<String> getConvertedLocations(List<String> locations) {
302                    List<String> converted = new ArrayList<String>();
303                    for (String location : locations) {
304                            if (LocationUtils.isExistingFile(location)) {
305                                    File file = new File(location);
306                                    // ClassPathXmlApplicationContext needs a fully qualified URL, not a filename
307                                    String url = LocationUtils.getCanonicalURLString(file);
308                                    converted.add(url);
309                            } else {
310                                    converted.add(location);
311                            }
312                    }
313                    return converted;
314            }
315    
316    }