Question

I need to dump content of some database tables to XML file with this structure

<dump>
  <table name="tableName1">
    <records>
      <record>
        <first column name>value</first column name>
        <second column name>value</second column name>
        <third column name>value</third column name>
      </record>
      <record>...</record>
    </records>
  </table>
  <table name="tableName2">...</table>
</dump>

The real number of records for every table in unknown, so I can't store all data for a single table in memory and dump to XML.
My job now is defined as:


<job id="dump-database-job">
  <step id="dumpTables">
  <tasklet>
    <chunk reader="dumpReader" processor="dumpProcessor" writer="dumpWriter" commit-interval="100" />
  </tasklet>
  </step>
</job>

<bean name="dumpProcessor" class="RecordBeanToJaxbElementProcessor" />
<bean name="dumpReader" class="CompositeItemReader">
  <property name="delegates">
  <array>
    <ref bean="TABLE_ONE_Reader" />
    <ref bean="TABLE_TWO_Reader" />
    <ref bean="TABLE_NTH_Reader" />
    <!-- Other delegates omitted,one for table,for brevity... -->
  </array>
  </property>
  <property name="name" value="dumpReader" />
</bean>

<bean name="TABLE_ONE_Reader" class="JdbcCursorItemReader">
  <property name="rowMapper">
    <bean name="rowMapper" class="RecordBeanRowMapper">
      <property name="tableName=" value="TABLE_ONE" />
     </bean>
   </property>
   <!--other mandatory property omitted -->
</bean>

<bean name="dumpWriter" class="StaxEventItemWriter" scope="step">
  <property name="resource" value="file:#{jobParameters['outfile']}" />
  <property name="shouldDeleteIfEmpty" value="true" />
  <property name="marshaller" ref="marshaller" />
  <property name="overwriteOutput" value="true" />
  <property name="rootTagName" value="dump" />
</bean>

<bean name="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
  <property name="supportJaxbElementClass" value="true" />
  <property name="classesToBeBound">
    <array>
      <value>RecordBean</value>
    </array>
  </property>
</bean>  

public class RecordBeanRowMapper implements RowMapper<RecordBean> {
  final static RowMapper<Map<String, Object>> columnMapRowMapper = new ColumnMapRowMapper();
  private String tableName;

  public void setTableName(String tableName) {
    this.tableName = tableName;
  }

  @Override
  public RecordBean mapRow(ResultSet rs, int rowNum) throws SQLException {
    final RecordBean b = new RecordBean();

    b.setTableName(tableName);
    b.setColumnValues(Maps.transformValues(columnMapRowMapper.mapRow(rs, rowNum), new Function<Object, String>()    {
      @Override
      public String apply(Object input) {
        return (input == null ? "NULL" : input.toString();
      }
  }
}

@XmlAccessorType(XmlAccessType.FIELD)
@XmlRootElement(namespace="")
class RecordBean {
  private String tableName;
  @XmlJavaTypeAdapter
  /* Entries are written using an adapter to write data as
   * <key>value</key>
   * <key>value</key>
   * ...
   */
  public Map<String,String> entries = new HashMap<String,String>();
}

/* Use to build item node name using dynamic tableName as
 * <tableName>
 *   <key>value</key>
 *   <key>value</key>
 * </tableName>
 */
public class RecordBeanToJaxbElementProcessor implements ItemProcessor<RecordBean, JAXBElement<?>> {
  @Override
  public JAXBElement<?> process(RecordBean item) throws Exception {
    return new JAXBElement<RecordBean>(new QName(item.getTableName()), RecordBean.class, item);
  }
}

This job is incomplete and doesn't cover my needs because the output looks like

<?xml version="1.0" encoding="UTF-8"?>
<dump>
  <!-- First record of table TABLE_ONE -->
  <TABLE_ONE>
    <code>Code one</code>
    <description>A record</description>
    <agomappedtable>xyz</agomappedtable>
    <enumcode>NULL</enumcode>
    <is_persistent>false</is_persistent>
    <keep_history_data>false</keep_history_data>
  </TABLE_ONE>
  <!-- Other tons from TABLE_ONE -->
  <!-- First record of table TABLE_TWO -->
  <TABLE_TWO>
    <code>Code 2</code>
    <description>Another record</description>
    <his_name>no_name</his_name>
  </TABLE_TWO>
  <!-- Other tons from TABLE_TWO -->
  <!-- More tables... -->
</dump>

I think I have to enrich writer and/or marshaller components to achieve my goal, but I haven't found a good way to proceed :(

My questions is:
How to build a complex XML structure as described at beginning and make the job fully restartable and with small memory usage?

Was it helpful?

Solution

[Taken from spring.io forum]
This isn't possibly with the default StaxEventItemWriter. I had to do something similar for one of my projects and wrote a custom GroupingStaxEventItemWriter. See the code below. You'll need to modify the openGroup and closeGroup method for your particular use case. Note that the grouping tag is closed by directly writing to the underlying java.io.Writer instead of through the XMLEventWriter. This is required for restartability.

public class GroupingStaxEventItemWriter<T> extends StaxEventItemWriter<T> {

    private static final String GROUP_IDENTIFIER = "CURRENT_GROUP";

    private Classifier<T, String> classifier;

    private String currentGroup;

    private XMLEventWriter eventWriter;

    private Writer writer;

    @Override
    public void write(List<? extends T> items) throws XmlMappingException, Exception {
        Map<String, List<T>> itemsGroup = new LinkedHashMap<String, List<T>>();
        for (T item : items) {
            String group = classifier.classify(item);
            if (!itemsGroup.containsKey(group)) {
                itemsGroup.put(group, new ArrayList<T>());
            }
            itemsGroup.get(group).add(item);
        }
        for (String group : itemsGroup.keySet()) {
            if (group == null || !group.equals(currentGroup)) {
                if (currentGroup != null) {
                    closeGroup(currentGroup);
                }
                currentGroup = group;
                if (currentGroup != null) {
                    openGroup(currentGroup);
                }
            }
            super.write(itemsGroup.get(group));
        }
    }

    protected void openGroup(String group) throws XMLStreamException, FactoryConfigurationError {
        String groupTagName = group;
        String groupTagNameSpacePrefix = "";
        String groupTagNameSpace = null;
        if (groupTagName.contains("{")) {
            groupTagNameSpace = groupTagName.replaceAll("\\{(.*)\\}.*", "$1");
            groupTagName = groupTagName.replaceAll("\\{.*\\}(.*)", "$1");
            if (groupTagName.contains(":")) {
                groupTagNameSpacePrefix = groupTagName.replaceAll("(.*):.*", "$1");
                groupTagName = groupTagName.replaceAll(".*:(.*)", "$1");
            }
        }
        XMLEventFactory xmlEventFactory = createXmlEventFactory();
        eventWriter.add(xmlEventFactory.createStartElement(groupTagNameSpacePrefix, groupTagNameSpace, groupTagName));
    }

    protected void closeGroup(String group)
            throws XMLStreamException, FactoryConfigurationError {
        String groupTagName = group;
        String groupTagNameSpacePrefix = "";
        if (groupTagName.contains("{")) {
            groupTagName = groupTagName.replaceAll("\\{.*\\}(.*)", "$1");
            if (groupTagName.contains(":")) {
                groupTagNameSpacePrefix = groupTagName.replaceAll("(.*):.*", "$1") + ":";
                groupTagName = groupTagName.replaceAll(".*:(.*)", "$1");
            }
        }
        try {
            writer.write("</" + groupTagNameSpacePrefix + groupTagName + ">");
        } catch (IOException ioe) {
            throw new DataAccessResourceFailureException("Unable to close group: [" + group + "]", ioe);
        }
    }

    @Override
    protected XMLEventWriter createXmlEventWriter(XMLOutputFactory outputFactory, Writer writer)
            throws XMLStreamException {
        this.writer = writer;
        this.eventWriter = super.createXmlEventWriter(outputFactory, writer);
        return eventWriter;
    }

    @Override
    public void open(ExecutionContext executionContext) {
        if (executionContext.containsKey(getExecutionContextKey(GROUP_IDENTIFIER))) {
            currentGroup = executionContext.getString(getExecutionContextKey(GROUP_IDENTIFIER));
        }
        super.open(executionContext);
    }

    @Override
    public void update(ExecutionContext executionContext) {
        executionContext.putString(getExecutionContextKey(GROUP_IDENTIFIER), currentGroup);
        super.update(executionContext);
    }

    @Override
    public void close() {
        if (currentGroup != null) {
            try {
                closeGroup(currentGroup);
            } catch (XMLStreamException e) {
                throw new ItemStreamException("Failed to write close tag for element: " + currentGroup, e);
            } catch (FactoryConfigurationError e) {
                throw new ItemStreamException("Failed to write close tag for element: " + currentGroup, e);
            }
        }
        super.close();
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        super.afterPropertiesSet();
        Assert.notNull(classifier, "Missing required property 'classifier'");
    }

    public void setClassifier(Classifier<T, String> classifier) {
        this.classifier = classifier;
    }

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top