001/*
002 * Copyright 2005-2014 the original author or authors.
003 *
004 * Licensed under the Apache 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 *      https://www.apache.org/licenses/LICENSE-2.0
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
017package org.springframework.batch.item.ldif;
018
019import org.slf4j.Logger;
020import org.slf4j.LoggerFactory;
021import org.springframework.batch.item.file.ResourceAwareItemReaderItemStream;
022import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader;
023import org.springframework.beans.factory.InitializingBean;
024import org.springframework.core.io.Resource;
025import org.springframework.ldap.core.LdapAttributes;
026import org.springframework.ldap.ldif.parser.LdifParser;
027import org.springframework.util.Assert;
028import org.springframework.util.ClassUtils;
029
030/**
031 * The {@link LdifReader LdifReader} is an adaptation of the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}
032 * built around an {@link LdifParser LdifParser}.
033 * <p>
034 * Unlike the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}, the {@link LdifReader LdifReader}
035 * does not require a mapper. Instead, this version of the {@link LdifReader LdifReader} simply returns an {@link LdapAttributes LdapAttributes}
036 * object which can be consumed and manipulated as necessary by {@link org.springframework.batch.item.ItemProcessor ItemProcessor} or any
037 * output service. Alternatively, the {@link RecordMapper RecordMapper} interface can be implemented and set in a
038 * {@link MappingLdifReader MappingLdifReader} to map records to objects for return.
039 * <p>
040 * {@link LdifReader LdifReader} usage is mimics that of the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}
041 * for all intensive purposes. Adjustments have been made to process records instead of lines, however.  As such, the
042 * {@link #recordsToSkip recordsToSkip} attribute indicates the number of records from the top of the file that should not be processed.
043 * Implementations of the {@link RecordCallbackHandler RecordCallbackHandler} interface can be used to execute operations on those skipped records.
044 * <p>
045 * As with the {@link org.springframework.batch.item.file.FlatFileItemReader FlatFileItemReader}, the {@link #strict strict} option differentiates
046 * between whether or not to require the resource to exist before processing.  In the case of a value set to false, a warning is logged instead of
047 * an exception being thrown.
048 *
049 * @author Keith Barlow
050 *
051 */
052public class LdifReader extends AbstractItemCountingItemStreamItemReader<LdapAttributes>
053                implements ResourceAwareItemReaderItemStream<LdapAttributes>, InitializingBean {
054
055        private static final Logger LOG = LoggerFactory.getLogger(LdifReader.class);
056
057        private Resource resource;
058
059        private LdifParser ldifParser;
060
061        private int recordCount = 0;
062
063        private int recordsToSkip = 0;
064
065        private boolean strict = true;
066
067        private RecordCallbackHandler skippedRecordsCallback;
068
069        public LdifReader() {
070                setName(ClassUtils.getShortName(LdifReader.class));
071        }
072
073        /**
074         * In strict mode the reader will throw an exception on
075         * {@link #open(org.springframework.batch.item.ExecutionContext)} if the
076         * input resource does not exist.
077         * @param strict true by default
078         */
079        public void setStrict(boolean strict) {
080                this.strict = strict;
081        }
082
083        /**
084         * {@link RecordCallbackHandler RecordCallbackHandler} implementations can be used to take action on skipped records.
085         *
086         * @param skippedRecordsCallback will be called for each one of the initial
087         * skipped lines before any items are read.
088         */
089        public void setSkippedRecordsCallback(RecordCallbackHandler skippedRecordsCallback) {
090                this.skippedRecordsCallback = skippedRecordsCallback;
091        }
092
093        /**
094         * Public setter for the number of lines to skip at the start of a file. Can
095         * be used if the file contains a header without useful (column name)
096         * information, and without a comment delimiter at the beginning of the
097         * lines.
098         *
099         * @param recordsToSkip the number of lines to skip
100         */
101        public void setRecordsToSkip(int recordsToSkip) {
102                this.recordsToSkip = recordsToSkip;
103        }
104
105        @Override
106        protected void doClose() throws Exception {
107                if (ldifParser != null) {
108                        ldifParser.close();
109                }
110                this.recordCount = 0;
111        }
112
113        @Override
114        protected void doOpen() throws Exception {
115                if (resource == null)
116                        throw new IllegalStateException("A resource has not been set.");
117
118                if (!resource.exists()) {
119                        if (strict) {
120                                throw new IllegalStateException("Input resource must exist (reader is in 'strict' mode): "+resource);
121                        } else {
122                                LOG.warn("Input resource does not exist " + resource.getDescription());
123                                return;
124                        }
125                }
126
127                ldifParser.open();
128
129                for (int i = 0; i < recordsToSkip; i++) {
130                        LdapAttributes record = ldifParser.getRecord();
131                        if (skippedRecordsCallback != null) {
132                                skippedRecordsCallback.handleRecord(record);
133                        }
134                }
135        }
136
137        @Override
138        protected LdapAttributes doRead() throws Exception {
139                LdapAttributes attributes = null;
140
141                try {
142                        if (ldifParser != null) {
143                                while (attributes == null && ldifParser.hasMoreRecords()) {
144                                        attributes = ldifParser.getRecord();
145                                }
146                                recordCount++;
147                        }
148
149                        return attributes;
150
151                } catch(Exception ex){
152                        LOG.error("Parsing error at record " + recordCount + " in resource=" +
153                                                          resource.getDescription() + ", input=[" + attributes + "]", ex);
154                        throw ex;
155                }
156        }
157
158        /**
159         * Establishes the resource that will be used as the input for the LdifReader.
160         *
161         * @param resource the resource that will be read.
162         */
163        public void setResource(Resource resource) {
164                this.resource = resource;
165                this.ldifParser = new LdifParser(resource);
166        }
167
168        public void afterPropertiesSet() throws Exception {
169                Assert.notNull(resource, "A resource is required to parse.");
170                Assert.notNull(ldifParser, "A parser is required");
171        }
172
173}