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