/** * AnalyzerBeans * Copyright (C) 2014 Neopost - Customer Information Management * * This copyrighted material is made available to anyone wishing to use, modify, * copy, or redistribute it subject to the terms and conditions of the GNU * Lesser General Public License, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this distribution; if not, write to: * Free Software Foundation, Inc. * 51 Franklin Street, Fifth Floor * Boston, MA 02110-1301 USA */ package org.eobjects.analyzer.job.builder; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import org.eobjects.analyzer.beans.api.Analyzer; import org.eobjects.analyzer.beans.api.ColumnProperty; import org.eobjects.analyzer.data.InputColumn; import org.eobjects.analyzer.descriptors.AnalyzerBeanDescriptor; import org.eobjects.analyzer.descriptors.ConfiguredPropertyDescriptor; import org.eobjects.analyzer.job.AnalysisJobImmutabilizer; import org.eobjects.analyzer.job.AnalyzerJob; import org.eobjects.analyzer.job.ComponentConfigurationException; import org.eobjects.analyzer.job.ComponentRequirement; import org.eobjects.analyzer.job.ImmutableAnalyzerJob; import org.eobjects.analyzer.job.ImmutableBeanConfiguration; import org.eobjects.analyzer.util.LabelUtils; import org.eobjects.analyzer.util.ReflectionUtils; import org.apache.metamodel.schema.Table; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * A builder of {@link AnalyzerJob} objects. * * @param <A> * the type of {@link Analyzer} that is being built. */ public final class AnalyzerJobBuilder<A extends Analyzer<?>> extends AbstractBeanWithInputColumnsBuilder<AnalyzerBeanDescriptor<A>, A, AnalyzerJobBuilder<A>> { private static final Logger logger = LoggerFactory.getLogger(AnalysisJobBuilder.class); /** * Field that determines if this analyzer is applicable for building * multiple jobs where the input columns have been partitioned based on * input size (single or multiple) and originating table */ private final boolean _multipleJobsSupported; private final List<InputColumn<?>> _inputColumns; private final ConfiguredPropertyDescriptor _inputProperty; private final List<AnalyzerChangeListener> _localChangeListeners; public AnalyzerJobBuilder(AnalysisJobBuilder analysisJobBuilder, AnalyzerBeanDescriptor<A> descriptor) { super(analysisJobBuilder, descriptor, AnalyzerJobBuilder.class); Set<ConfiguredPropertyDescriptor> inputProperties = descriptor.getConfiguredPropertiesForInput(false); if (inputProperties.size() == 1) { _inputProperty = inputProperties.iterator().next(); final ColumnProperty columnProperty = _inputProperty.getAnnotation(ColumnProperty.class); _multipleJobsSupported = columnProperty != null && columnProperty.escalateToMultipleJobs(); _inputColumns = new ArrayList<InputColumn<?>>(); } else { _multipleJobsSupported = false; _inputColumns = null; _inputProperty = null; } _localChangeListeners = new ArrayList<AnalyzerChangeListener>(0); } /** * Builds a temporary list of all listeners, both global and local * * @return */ private List<AnalyzerChangeListener> getAllListeners() { List<AnalyzerChangeListener> globalChangeListeners = getAnalysisJobBuilder().getAnalyzerChangeListeners(); List<AnalyzerChangeListener> list = new ArrayList<AnalyzerChangeListener>(globalChangeListeners.size() + _localChangeListeners.size()); list.addAll(globalChangeListeners); list.addAll(_localChangeListeners); return list; } public boolean isMultipleJobsDeterminedBy(ConfiguredPropertyDescriptor propertyDescriptor) { return _multipleJobsSupported && propertyDescriptor.isInputColumn() && propertyDescriptor.isRequired(); } public AnalyzerJob toAnalyzerJob() throws IllegalStateException { return toAnalyzerJob(true); } public AnalyzerJob toAnalyzerJob(boolean validate) throws IllegalStateException { AnalyzerJob[] analyzerJobs = toAnalyzerJobs(); if (analyzerJobs == null || analyzerJobs.length == 0) { return null; } if (validate && analyzerJobs.length > 1) { throw new IllegalStateException("This builder generates " + analyzerJobs.length + " jobs, but a single job was requested"); } return analyzerJobs[0]; } public AnalyzerJob[] toAnalyzerJobs() throws IllegalStateException { return toAnalyzerJobs(true); } public AnalyzerJob[] toAnalyzerJobs(AnalysisJobImmutabilizer immutabilizer) throws IllegalStateException { return toAnalyzerJobs(true, immutabilizer); } public AnalyzerJob[] toAnalyzerJobs(boolean validate) throws IllegalStateException { return toAnalyzerJobs(validate, new AnalysisJobImmutabilizer()); } public AnalyzerJob[] toAnalyzerJobs(boolean validate, AnalysisJobImmutabilizer immutabilizer) throws IllegalStateException { final Map<ConfiguredPropertyDescriptor, Object> configuredProperties = getConfiguredProperties(); final ComponentRequirement componentRequirement = immutabilizer.load(getComponentRequirement()); if (!_multipleJobsSupported) { ImmutableAnalyzerJob job = new ImmutableAnalyzerJob(getName(), getDescriptor(), new ImmutableBeanConfiguration(configuredProperties), componentRequirement, getMetadataProperties()); return new AnalyzerJob[] { job }; } if (validate && _inputColumns.isEmpty()) { throw new IllegalStateException("No input column configured"); } final List<InputColumn<?>> tableLessColumns = new ArrayList<InputColumn<?>>(); final Map<Table, List<InputColumn<?>>> originatingTables = new LinkedHashMap<Table, List<InputColumn<?>>>(); for (InputColumn<?> inputColumn : _inputColumns) { Table table = getAnalysisJobBuilder().getOriginatingTable(inputColumn); if (table == null) { // some columns (such as those based on an expression) don't // originate from a table. They should be applied to all jobs. tableLessColumns.add(inputColumn); } else { List<InputColumn<?>> list = originatingTables.get(table); if (list == null) { list = new ArrayList<InputColumn<?>>(); } list.add(inputColumn); originatingTables.put(table, list); } } if (validate && originatingTables.isEmpty()) { final List<Table> sourceTables = getAnalysisJobBuilder().getSourceTables(); if (sourceTables.size() == 1) { logger.info("Only a single source table is available, so the source of analyzer '{}' is inferred", this); Table table = sourceTables.get(0); originatingTables.put(table, new ArrayList<InputColumn<?>>()); } else { throw new IllegalStateException("Could not determine source for analyzer '" + this + "'"); } } if (originatingTables.size() == 1 && _inputProperty.isArray()) { // there's only a single table involved - leave the input columns // untouched ImmutableAnalyzerJob job = new ImmutableAnalyzerJob(getName(), getDescriptor(), new ImmutableBeanConfiguration(configuredProperties), componentRequirement, getMetadataProperties()); return new AnalyzerJob[] { job }; } for (Entry<Table, List<InputColumn<?>>> entry : originatingTables.entrySet()) { entry.getValue().addAll(tableLessColumns); } List<AnalyzerJob> jobs = new ArrayList<AnalyzerJob>(); Set<Entry<Table, List<InputColumn<?>>>> entrySet = originatingTables.entrySet(); for (Iterator<Entry<Table, List<InputColumn<?>>>> iterator = entrySet.iterator(); iterator.hasNext();) { Entry<Table, List<InputColumn<?>>> entry = (Entry<Table, List<InputColumn<?>>>) iterator.next(); List<InputColumn<?>> columns = entry.getValue(); if (_inputProperty.isArray()) { jobs.add(createPartitionedJob(columns.toArray(new InputColumn[columns.size()]), configuredProperties)); } else { for (InputColumn<?> column : columns) { jobs.add(createPartitionedJob(column, configuredProperties)); } } } if (validate && !isConfigured()) { throw new IllegalStateException("Row processing Analyzer job is not correctly configured"); } return jobs.toArray(new AnalyzerJob[jobs.size()]); } @Override public AnalyzerJobBuilder<A> addInputColumn(InputColumn<?> inputColumn, ConfiguredPropertyDescriptor propertyDescriptor) { assert propertyDescriptor.isInputColumn(); if (inputColumn == null) { throw new IllegalArgumentException("InputColumn cannot be null"); } if (isMultipleJobsDeterminedBy(propertyDescriptor)) { _inputColumns.add(inputColumn); return this; } else { return super.addInputColumn(inputColumn, propertyDescriptor); } } @Override public boolean isConfigured(ConfiguredPropertyDescriptor configuredProperty, boolean throwException) { if (_multipleJobsSupported && configuredProperty == _inputProperty) { if (_inputColumns.isEmpty()) { Object propertyValue = super.getConfiguredProperty(configuredProperty); if (propertyValue != null) { if (propertyValue.getClass().isArray() && Array.getLength(propertyValue) > 0) { setConfiguredProperty(configuredProperty, propertyValue); return isConfigured(configuredProperty, throwException); } } if (throwException) { throw new ComponentConfigurationException("No input columns configured for " + LabelUtils.getLabel(this)); } else { return false; } } return true; } return super.isConfigured(configuredProperty, throwException); } private AnalyzerJob createPartitionedJob(Object columnValue, Map<ConfiguredPropertyDescriptor, Object> configuredProperties) { Map<ConfiguredPropertyDescriptor, Object> jobProperties = new HashMap<ConfiguredPropertyDescriptor, Object>( configuredProperties); jobProperties.put(_inputProperty, columnValue); ComponentRequirement componentRequirement = new AnalysisJobImmutabilizer().load(getComponentRequirement()); ImmutableAnalyzerJob job = new ImmutableAnalyzerJob(getName(), getDescriptor(), new ImmutableBeanConfiguration( jobProperties), componentRequirement, getMetadataProperties()); return job; } @Override public String toString() { return "AnalyzerJobBuilder[analyzer=" + getDescriptor().getDisplayName() + ",inputColumns=" + getInputColumns() + "]"; } @Override public AnalyzerJobBuilder<A> setConfiguredProperty(ConfiguredPropertyDescriptor configuredProperty, Object value) { if (isMultipleJobsDeterminedBy(configuredProperty)) { // the dummy value is used just to pass something to the underlying // prototype bean. final InputColumn<?> dummyValue; _inputColumns.clear(); if (ReflectionUtils.isArray(value)) { int length = Array.getLength(value); for (int i = 0; i < length; i++) { InputColumn<?> inputColumn = (InputColumn<?>) Array.get(value, i); _inputColumns.add(inputColumn); } if (_inputColumns.isEmpty()) { dummyValue = null; } else { dummyValue = _inputColumns.iterator().next(); } } else { InputColumn<?> col = (InputColumn<?>) value; _inputColumns.add(col); dummyValue = col; } if (configuredProperty.isArray()) { final InputColumn<?>[] inputColumsArray; if (dummyValue == null) { inputColumsArray = new InputColumn[0]; } else { inputColumsArray = new InputColumn[] { dummyValue }; } return super.setConfiguredProperty(configuredProperty, inputColumsArray); } else { return super.setConfiguredProperty(configuredProperty, dummyValue); } } else { return super.setConfiguredProperty(configuredProperty, value); } } @Override public Object getConfiguredProperty(ConfiguredPropertyDescriptor propertyDescriptor) { if (isMultipleJobsDeterminedBy(propertyDescriptor)) { return _inputColumns.toArray(new InputColumn[_inputColumns.size()]); } else { return super.getConfiguredProperty(propertyDescriptor); } } @Override public void onConfigurationChanged() { super.onConfigurationChanged(); List<AnalyzerChangeListener> listeners = getAllListeners(); for (AnalyzerChangeListener listener : listeners) { listener.onConfigurationChanged(this); } } @Override public void onRequirementChanged() { super.onRequirementChanged(); List<AnalyzerChangeListener> listeners = getAllListeners(); for (AnalyzerChangeListener listener : listeners) { listener.onRequirementChanged(this); } } public boolean isMultipleJobsSupported() { return _multipleJobsSupported; } /** * Notification method invoked when transformer is removed. */ protected void onRemoved() { List<AnalyzerChangeListener> listeners = getAllListeners(); for (AnalyzerChangeListener listener : listeners) { listener.onRemove(this); } } /** * Adds a change listener to this component * * @param listener */ public void addChangeListener(AnalyzerChangeListener listener) { _localChangeListeners.add(listener); } /** * Removes a change listener from this component * * @param listener * @return whether or not the listener was found and removed. */ public boolean removeChangeListener(AnalyzerChangeListener listener) { return _localChangeListeners.remove(listener); } }