001/**
002 * Copyright 2005-2016 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 */
016package org.kuali.rice.xml.ingest;
017
018import java.util.Collection;
019import java.util.Collections;
020import java.util.List;
021
022import org.apache.commons.collections.CollectionUtils;
023import org.apache.commons.lang3.StringUtils;
024import org.kuali.common.util.FormatUtils;
025import org.kuali.common.util.LocationUtils;
026import org.kuali.common.util.execute.Executable;
027import org.kuali.common.util.log.LoggerUtils;
028import org.kuali.rice.core.api.CoreApiServiceLocator;
029import org.kuali.rice.core.api.impex.xml.LocationXmlDocCollection;
030import org.kuali.rice.core.api.impex.xml.XmlDocCollection;
031import org.kuali.rice.core.api.impex.xml.XmlIngesterService;
032import org.slf4j.Logger;
033
034import com.google.common.base.Optional;
035import com.google.common.base.Preconditions;
036import com.google.common.collect.Lists;
037
038/**
039 * Locates workflow XML documents available on the classpath and ingests them.
040 * 
041 * <p>
042 * No file system access is required. The XML documents are ingested using Spring's {@code classpath:} notation to
043 * locate them, open an {@link java.io.InputStream}, and feed them to the ingester service. Any workflow document
044 * failing to be ingested correctly results in an exception being thrown.
045 * </p>
046 * 
047 * <p>
048 * If an explicit {@link XmlIngesterService} instance is not provided,
049 * {@code CoreApiServiceLocator.getXmlIngesterService()} must be able to correctly locate {@link XmlIngesterService}.
050 * </p>
051 * 
052 * @author Kuali Rice Team (rice.collab@kuali.org)
053 */
054public final class IngestXmlExecutable implements Executable {
055
056        private static final Logger logger = LoggerUtils.make();
057
058        private static final String XML_SUFFIX = ".xml";
059
060        private final List<String> xmlDocumentLocations;
061        private final boolean skip;
062
063        private final Optional<XmlIngesterService> xmlIngesterService;
064
065    /**
066     * {@inheritDoc}
067     */
068        @Override
069        public void execute() {
070                if (skip) {
071                        logger.info("Skipping XML ingestion");
072                        return;
073                }
074
075                long start = System.currentTimeMillis();
076                logger.info("Starting XML Ingester.");
077
078        for (String xmlDocumentLocation : xmlDocumentLocations) {
079                    logger.info("Ingesting XML documents listed in [{}]", xmlDocumentLocation);
080        }
081
082                List<XmlDocCollection> xmlDocumentCollections = getXmlDocCollectionList(xmlDocumentLocations);
083                logger.info("Found {} files to ingest.", Integer.valueOf(xmlDocumentCollections.size()));
084
085                Collection<XmlDocCollection> failedXmlDocumentCollections = ingest(xmlDocumentCollections);
086                validateNoFailures(failedXmlDocumentCollections);
087                logger.info("There were zero failures ingesting {} XML documents", Integer.valueOf(xmlDocumentCollections.size()));
088
089        long end = System.currentTimeMillis() - start;
090                logger.info("Finished ingesting bootstrap XML - {}", FormatUtils.getTime(end));
091        }
092
093    /**
094     * Gets the list of XML documents to ingest.
095     *
096     * @param locationListings the locations to search for XML documents to ingest
097     *
098     * @return the list of XML documents to ingest
099     */
100        private List<XmlDocCollection> getXmlDocCollectionList(List<String> locationListings) {
101                List<XmlDocCollection> xmlDocCollectionList = Lists.newArrayList();
102                List<String> locations = LocationUtils.getLocations(locationListings);
103
104                for (String location : locations) {
105                        Preconditions.checkState(StringUtils.endsWith(location.toLowerCase(), XML_SUFFIX), "[%s] is not an XML document", location);
106                        Preconditions.checkState(LocationUtils.exists(location), "[%s] does not exist", location);
107
108            logger.info("[{}]", location);
109
110            xmlDocCollectionList.add(new LocationXmlDocCollection(location));
111                }
112
113                return xmlDocCollectionList;
114        }
115
116    /**
117     * Ingests the documents in {@code collections}.
118     *
119     * @param collections the list of XML documents to ingest
120     *
121     * @return the list of XML documents that failed to ingest
122     */
123    private Collection<XmlDocCollection> ingest(List<XmlDocCollection> collections) {
124        try {
125            return getXmlIngesterService().ingest(collections);
126        } catch (Exception e) {
127            throw new IllegalStateException("Unexpected error ingesting XML documents", e);
128        }
129    }
130
131    /**
132     * Verifies whether there are any failures in {@code failedXmlDocumentCollections} and lists them if there are.
133     *
134     * @param failedXmlDocumentCollections the list of failures from the ingestion process
135     */
136        private void validateNoFailures(Collection<XmlDocCollection> failedXmlDocumentCollections) {
137                if (failedXmlDocumentCollections.isEmpty()) {
138                        return;
139                }
140
141        List<String> failureNamesList = Lists.newArrayList();
142                for (XmlDocCollection failedXmlDocumentCollection : failedXmlDocumentCollections) {
143            failureNamesList.add(failedXmlDocumentCollection.getFile().getName());
144                }
145
146        String failureNames = StringUtils.join(failureNamesList, ", ");
147                Preconditions.checkState(false, "%s XML documents failed to ingest -> [%s]", Integer.valueOf(failedXmlDocumentCollections.size()), failureNames);
148        }
149
150    /**
151     * Returns the {@link XmlIngesterService}.
152     *
153     * @return the {@link XmlIngesterService}
154     */
155    public XmlIngesterService getXmlIngesterService() {
156        return xmlIngesterService.isPresent() ? xmlIngesterService.get() : CoreApiServiceLocator.getXmlIngesterService();
157    }
158
159    private IngestXmlExecutable(Builder builder) {
160        this.xmlDocumentLocations = builder.xmlDocumentLocations;
161        this.skip = builder.skip;
162        this.xmlIngesterService = builder.xmlIngesterService;
163    }
164
165    /**
166     * Returns the builder for this {@code IngestXmlExecutable}.
167     *
168     * @param xmlDocumentLocations the list of locations with XML documents to ingest
169     *
170     * @return the builder for this {@code IngestXmlExecutable}
171     */
172    public static Builder builder(List<String> xmlDocumentLocations) {
173        return new Builder(xmlDocumentLocations);
174    }
175
176    /**
177     * Builds this {@link IngestXmlExecutable}.
178     */
179        public static class Builder {
180
181                // Required
182                private final List<String> xmlDocumentLocations;
183
184                // Optional
185                private Optional<XmlIngesterService> xmlIngesterService = Optional.absent();
186                private boolean skip;
187
188        /**
189         * Builds the {@link IngestXmlExecutable} with a single {@code xmlDocumentLocation}.
190         *
191         * @param xmlDocumentLocation the location with an XML document to ingest
192         */
193                public Builder(String xmlDocumentLocation) {
194                        this.xmlDocumentLocations = Collections.singletonList(xmlDocumentLocation);
195        }
196
197        /**
198         * Builds the {@link IngestXmlExecutable} with multiple {@code xmlDocumentLocations}.
199         *
200         * @param xmlDocumentLocations the list of locations with XML documents to ingest
201         */
202        public Builder(List<String> xmlDocumentLocations) {
203            this.xmlDocumentLocations = xmlDocumentLocations;
204        }
205
206        /**
207         * Sets the {@link XmlIngesterService}.
208         *
209         * @param service the {@link XmlIngesterService} to set
210         *
211         * @return this {@code Builder}
212         */
213                public Builder service(XmlIngesterService service) {
214                        this.xmlIngesterService = Optional.of(service);
215                        return this;
216                }
217
218        /**
219         * Sets whether to skip this executable or not.
220         *
221         * @param skip whether to skip this executable or not
222         *
223         * @return this {@code Builder}
224         */
225                public Builder skip(boolean skip) {
226                        this.skip = skip;
227                        return this;
228                }
229
230        /**
231         * Builds the {@link IngestXmlExecutable}.
232         *
233         * @return the built {@link IngestXmlExecutable}
234         */
235                public IngestXmlExecutable build() {
236                        IngestXmlExecutable instance = new IngestXmlExecutable(this);
237
238            validate(instance);
239
240            return instance;
241                }
242
243                private static void validate(IngestXmlExecutable instance) {
244                        Preconditions.checkNotNull(instance.xmlIngesterService, "service cannot be null");
245                        Preconditions.checkArgument(!CollectionUtils.isEmpty(instance.xmlDocumentLocations), "locationListings cannot be empty");
246
247            for (String locationListing : instance.xmlDocumentLocations) {
248                Preconditions.checkArgument(!StringUtils.isBlank(locationListing), "locationListings cannot have blank entries");
249                            Preconditions.checkArgument(LocationUtils.exists(locationListing), "[%s] does not exist", locationListing);
250            }
251                }
252
253        }
254
255}