=== added file 'dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/adx/ADXException.java' --- dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/adx/ADXException.java 1970-01-01 00:00:00 +0000 +++ dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/adx/ADXException.java 2015-07-03 16:31:04 +0000 @@ -0,0 +1,41 @@ +package org.hisp.dhis.dxf2.adx; + +/* + * Copyright (c) 2015, UiO + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * A simple wrapper for ADX checked exceptions + * + * @author bobj + */ +public class ADXException + extends Exception +{ + public ADXException(String msg) + { + super(msg); + } +} === modified file 'dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/adx/DefaultADXDataService.java' --- dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/adx/DefaultADXDataService.java 2015-06-19 14:55:47 +0000 +++ dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/adx/DefaultADXDataService.java 2015-07-03 16:31:04 +0000 @@ -28,20 +28,22 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import com.sun.org.apache.xml.internal.utils.XMLChar; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.PipedOutputStream; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import java.util.logging.Level; -import java.util.logging.Logger; import javax.xml.stream.XMLOutputFactory; import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; import javax.xml.stream.XMLStreamException; @@ -53,6 +55,20 @@ import org.amplecode.staxwax.reader.XMLReader; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hisp.dhis.common.IdentifiableProperty; +import org.hisp.dhis.dataelement.CategoryComboMap; +import org.hisp.dhis.dataelement.CategoryComboMap.CategoryComboMapException; +import org.hisp.dhis.dataelement.DataElement; +import org.hisp.dhis.dataelement.DataElementCategory; +import org.hisp.dhis.dataelement.DataElementCategoryCombo; +import org.hisp.dhis.dataelement.DataElementCategoryOptionCombo; +import org.hisp.dhis.dataelement.DataElementCategoryService; +import org.hisp.dhis.dataelement.DataElementService; +import org.hisp.dhis.dataset.DataSet; +import org.hisp.dhis.dataset.DataSetService; +import org.hisp.dhis.dxf2.adx.ADXPeriod.ADXPeriodException; import org.hisp.dhis.dxf2.datavalueset.PipedImporter; import org.hisp.dhis.dxf2.importsummary.ImportStatus; import org.hisp.dhis.dxf2.importsummary.ImportSummary; @@ -66,24 +82,60 @@ public class DefaultADXDataService implements ADXDataService { - @Autowired - protected DataValueSetService dataValueSetService; + private static final Log log = LogFactory.getLog( DefaultADXDataService.class ); - protected ExecutorService executor; + // ------------------------------------------------------------------------- + // Constants + // ------------------------------------------------------------------------- public static final int PIPE_BUFFER_SIZE = 4096; public static final int TOTAL_MINUTES_TO_WAIT = 5; + // ------------------------------------------------------------------------- + // Dependencies + // ------------------------------------------------------------------------- + + @Autowired + protected DataValueSetService dataValueSetService; + public void setDataValueSetService( DataValueSetService dataValueSetService ) { this.dataValueSetService = dataValueSetService; } + @Autowired + protected DataElementService dataElementService; + + public void setDataElementService( DataElementService dataElementService ) + { + this.dataElementService = dataElementService; + } + + @Autowired + protected DataElementCategoryService categoryService; + + public void setCategoryService( DataElementCategoryService categoryService ) + { + this.categoryService = categoryService; + } + + @Autowired + protected DataSetService dataSetService; + + public void setDataSetService( DataSetService dataSetService ) + { + this.dataSetService = dataSetService; + } + + // ------------------------------------------------------------------------- + // Public methods + // ------------------------------------------------------------------------- + @Override public void getData( DataExportParams params, OutputStream out ) { - throw new UnsupportedOperationException( "Not supported yet." ); + throw new UnsupportedOperationException( "ADX export not supported yet." ); } @Override @@ -94,12 +146,16 @@ XMLReader adxReader = XMLFactory.getXMLReader( in ); ImportSummaries importSummaries = new ImportSummaries(); - + adxReader.moveToStartElement( ADXConstants.ROOT, ADXConstants.NAMESPACE ); - - // TODO: inject this? - executor = Executors.newSingleThreadExecutor(); - + + Set attributeCategories = new HashSet<>(categoryService.getAttributeCategories()); + + Set categories = new HashSet<>(categoryService.getAllDataElementCategories()); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + + // submit each ADX group to DXF importer as a datavalueSet while ( adxReader.moveToStartElement( ADXConstants.GROUP, ADXConstants.NAMESPACE ) ) { try (PipedOutputStream pipeOut = new PipedOutputStream()) @@ -108,67 +164,266 @@ futureImportSummary = executor.submit(new PipedImporter( dataValueSetService, importOptions, pipeOut ) ); XMLOutputFactory factory = XMLOutputFactory.newInstance(); XMLStreamWriter dxfWriter = factory.createXMLStreamWriter( pipeOut ); - parseADXGroupToDxf( adxReader, dxfWriter ); + parseADXGroupToDxf( adxReader, dxfWriter, importOptions ); pipeOut.flush(); - importSummaries.addImportSummary( futureImportSummary.get( TOTAL_MINUTES_TO_WAIT, TimeUnit.SECONDS ) ); + importSummaries.addImportSummary( futureImportSummary.get( TOTAL_MINUTES_TO_WAIT, TimeUnit.MINUTES ) ); } - catch ( IOException | XMLStreamException | InterruptedException | ExecutionException | TimeoutException ex ) + catch ( IOException | XMLStreamException | InterruptedException | + ExecutionException | TimeoutException |ADXException | ADXPeriodException ex ) { ImportSummary importSummary = new ImportSummary(); importSummary.setStatus( ImportStatus.ERROR ); importSummary.setDescription( "Exception: " + ex.getMessage() ); importSummaries.addImportSummary( importSummary ); - Logger.getLogger( DefaultADXDataService.class.getName() ).log( Level.SEVERE, null, ex ); - } + log.warn( "Import failed: " + ex ); + } } + + executor.shutdown(); return importSummaries; } - protected void parseADXGroupToDxf( XMLReader adxReader, XMLStreamWriter dxfWriter ) throws XMLStreamException + // ------------------------------------------------------------------------- + // Utitility methods + // ------------------------------------------------------------------------- + + protected void parseADXGroupToDxf( XMLReader adxReader, XMLStreamWriter dxfWriter, ImportOptions importOptions ) + throws XMLStreamException, ADXException, ADXPeriodException { dxfWriter.writeStartDocument( "1.0" ); dxfWriter.writeStartElement( "dataValueSet" ); dxfWriter.writeDefaultNamespace( "http://dhis2.org/schema/dxf/2.0" ); + IdentifiableProperty dataElementIdScheme = importOptions.getDataElementIdScheme(); + Map groupAttributes = readAttributes( adxReader ); + if (!groupAttributes.containsKey( "period")) + { + throw new ADXException("'period' attribute is required on 'group'"); + } + + if (!groupAttributes.containsKey( "orgUnit")) + { + throw new ADXException("'orgUnit' attribute is required on 'group'"); + } + + // translate adx period to dxf2 String periodStr = groupAttributes.get( ADXConstants.PERIOD ); groupAttributes.remove( ADXConstants.PERIOD ); - Period period = ADXPeriod.parse( periodStr ); dxfWriter.writeAttribute( "period", period.getIsoDate() ); + + // process adx group attributes + if (!groupAttributes.containsKey( "attributeOptionCombo") && groupAttributes.containsKey( "dataSet")) + { + log.debug( "No attributeOptionCombo present. Check dataSet for attribute categorycombo"); + + DataSet dataSet = getDataSetByIdentifier( groupAttributes.get( "dataSet"), dataElementIdScheme ); + groupAttributes.put( "dataSet", dataSet.getUid()); + DataElementCategoryCombo attributeCombo = dataSet.getCategoryCombo(); + attributesToDXF("attributeOptionCombo", attributeCombo, groupAttributes, dataElementIdScheme); + } - // pass through the remaining attributes to dxf + // write the remaining attributes through to dxf stream for ( String attribute : groupAttributes.keySet() ) { dxfWriter.writeAttribute( attribute, groupAttributes.get( attribute ) ); } - + + // process the dataValues while ( adxReader.moveToStartElement( ADXConstants.DATAVALUE, ADXConstants.GROUP ) ) { - parseADXDataValueToDxf( adxReader, dxfWriter ); + parseADXDataValueToDxf( adxReader, dxfWriter, importOptions ); } + dxfWriter.writeEndElement(); dxfWriter.writeEndDocument(); } - protected void parseADXDataValueToDxf( XMLReader adxReader, XMLStreamWriter dxfWriter ) throws XMLStreamException + protected void parseADXDataValueToDxf( XMLReader adxReader, XMLStreamWriter dxfWriter, ImportOptions importOptions ) + throws XMLStreamException, ADXException { + Map dvAttributes = readAttributes( adxReader ); + + if (!dvAttributes.containsKey( "dataElement")) + { + throw new ADXException("'dataElement' attribute is required on 'dataValue'"); + } + + if (!dvAttributes.containsKey( "value")) + { + throw new ADXException("'value' attribute is required on 'dataValue'"); + } + + IdentifiableProperty dataElementIdScheme = importOptions.getDataElementIdScheme(); + dxfWriter.writeStartElement( "dataValue" ); - - Map groupAttributes = readAttributes( adxReader ); - + + DataElement dataElement = getDataElementByIdentifier( dvAttributes.get( "dataElement"), dataElementIdScheme ); + DataElementCategoryCombo categoryCombo = dataElement.getCategoryCombo(); + + attributesToDXF("categoryOptionCombo", categoryCombo, dvAttributes, dataElementIdScheme); + + // if dataelement type is string we need to pick out the 'annotation' element + if (dataElement.getType().equals( DataElement.VALUE_TYPE_STRING )) + { + adxReader.moveToStartElement( "annotation", "datavalue"); + if (adxReader.isStartElement("annotation")) + { + String textValue = adxReader.getElementValue(); + dvAttributes.put( "value", textValue); + } + else + { + throw new ADXException("Dataelement " + dataElement.getShortName() + " expects text annotation"); + } + } // pass through the remaining attributes to dxf - for ( String attribute : groupAttributes.keySet() ) + for ( String attribute : dvAttributes.keySet() ) { - dxfWriter.writeAttribute( attribute, groupAttributes.get( attribute ) ); + dxfWriter.writeAttribute( attribute, dvAttributes.get( attribute ) ); } dxfWriter.writeEndElement(); } - - // TODO this should really be part of staxwax library + + protected Map createCategoryMap(DataElementCategoryCombo catcombo) + throws ADXException + { + Map categoryMap = new HashMap<>(); + + List categories = catcombo.getCategories(); + + for (DataElementCategory category : categories) + { + String categoryCode = category.getCode(); + if (categoryCode==null || !XMLChar.isValidName( categoryCode )) + { + throw new ADXException("Category code for " + category.getName() + " is missing or invalid: "+ categoryCode); + } + categoryMap.put( category.getCode(), category); + } + + return categoryMap; + } + + protected DataElementCategoryOptionCombo getCatOptComboFromAttributes (Map attributes, + DataElementCategoryCombo catcombo, IdentifiableProperty scheme) + throws ADXException + { + + CategoryComboMap catcomboMap; + try + { + catcomboMap = new CategoryComboMap(catcombo, scheme); + log.debug(catcomboMap.toString()); + + } catch ( CategoryComboMapException ex ) + { + log.warn("Failed to create catcomboMap from " + catcombo); + throw new ADXException(ex.getMessage()); + } + + String compositeIdentifier =""; + for (DataElementCategory category : catcomboMap.getCategories()) + { + String catCode = category.getCode(); + if (catCode == null) + throw new RuntimeException("No category matching " + catCode); + String catAttribute = attributes.get(catCode); + if (catAttribute == null) + throw new RuntimeException("Missing required attribute from catcombo: " + catCode); + compositeIdentifier += "\"" + catAttribute + "\""; + } + DataElementCategoryOptionCombo catoptcombo = catcomboMap.getCategoryOptionCombo(compositeIdentifier); + + if (catoptcombo == null) + { + throw new ADXException("Invalid attributes:" + attributes); + } + return catoptcombo; + } + + protected void attributesToDXF( String optionComboName, DataElementCategoryCombo catCombo, Map attributes, IdentifiableProperty scheme ) + throws ADXException + { + log.debug("adx attributes: " + attributes); + + if (catCombo == categoryService.getDefaultDataElementCategoryCombo()) + { + // nothing to do + return; + } + Map categoryMap = createCategoryMap(catCombo); + + Map attributeOptions = new HashMap<>(); + Set attributeKeys = attributes.keySet(); + for (String category : categoryMap.keySet()) + { + if (attributes.containsKey( category)) + { + attributeOptions.put( category, attributes.get( category) ); + attributes.remove( category ); + } + else + { + throw new ADXException ("catcombo " + catCombo.getName() + " must have " + categoryMap.get( category).getName()); + } + } + + DataElementCategoryOptionCombo catOptCombo = getCatOptComboFromAttributes( attributeOptions, catCombo, scheme); + attributes.put( optionComboName, catOptCombo.getUid()); + + log.debug("dxf attributes: " + attributes); + } + + // ------------------------------------------------------------------------- + // The following methods are very general and should probably be implemented elsewhere + // ------------------------------------------------------------------------- + + protected DataSet getDataSetByIdentifier(String id, IdentifiableProperty scheme) throws ADXException + { + DataSet dataSet = null; + switch (scheme) { + case CODE: + dataSet = dataSetService.getDataSetByCode( id ); + break; + case UID: + dataSet = dataSetService.getDataSet(id ); + break; + default: + throw new ADXException("Only CODE or UID supported for dataSet identifier"); + } + + if (dataSet==null) + throw new ADXException("No dataSet matching " + id); + + return dataSet; + } + + protected DataElement getDataElementByIdentifier(String id, IdentifiableProperty scheme) throws ADXException + { + DataElement dataElement = null; + switch (scheme) { + case CODE: + dataElement = dataElementService.getDataElementByCode( id ); + break; + case UID: + dataElement = dataElementService.getDataElement(id ); + break; + default: + throw new ADXException("Only CODE or UID supported for dataElement identifier"); + } + + if (dataElement==null) + throw new ADXException("No dataElement matching " + id); + + return dataElement; + } + + // TODO this should be part of staxwax library protected Map readAttributes( XMLReader staxWaxReader ) throws XMLStreamException { Map attributes = new HashMap<>(); @@ -188,4 +443,5 @@ return attributes; } + } === modified file 'dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/datavalueset/PipedImporter.java' --- dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/datavalueset/PipedImporter.java 2015-06-19 21:34:53 +0000 +++ dhis-2/dhis-services/dhis-service-dxf2/src/main/java/org/hisp/dhis/dxf2/datavalueset/PipedImporter.java 2015-07-03 16:31:04 +0000 @@ -37,7 +37,6 @@ import org.hisp.dhis.dxf2.common.ImportOptions; import org.hisp.dhis.dxf2.importsummary.ImportStatus; import org.hisp.dhis.dxf2.importsummary.ImportSummary; -import org.hisp.dhis.user.CurrentUserService; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -61,7 +60,7 @@ private final ImportOptions importOptions; - private Authentication authentication; + private final Authentication authentication; public PipedImporter( DataValueSetService dataValueSetService, ImportOptions importOptions, PipedOutputStream pipeOut ) throws IOException === modified file 'dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/adx/DefaultADXDataServiceTest.java' --- dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/adx/DefaultADXDataServiceTest.java 2015-06-19 14:55:47 +0000 +++ dhis-2/dhis-services/dhis-service-dxf2/src/test/java/org/hisp/dhis/dxf2/adx/DefaultADXDataServiceTest.java 2015-07-03 16:31:04 +0000 @@ -30,7 +30,9 @@ import java.io.InputStream; import org.hisp.dhis.DhisSpringTest; +import static org.hisp.dhis.common.IdentifiableProperty.CODE; import org.hisp.dhis.datavalue.DataValue; +import org.hisp.dhis.dxf2.common.ImportOptions; import org.hisp.dhis.dxf2.datavalueset.DataValueSetService; import org.hisp.dhis.dxf2.importsummary.ImportSummaries; import org.hisp.dhis.jdbc.batchhandler.DataValueBatchHandler; @@ -75,7 +77,11 @@ { InputStream in = new ClassPathResource( SIMPLE_ADX_SAMPLE ).getInputStream(); - ImportSummaries importSummaries = adxDataService.postData(in, null); + ImportOptions options = new ImportOptions(); + options.setDataElementIdScheme( "CODE"); + options.setOrgUnitIdScheme( "CODE" ); + + ImportSummaries importSummaries = adxDataService.postData(in, options); assertEquals(importSummaries.getImportSummaries().size(), 2); // only testing this far .. summaries are full of conflicts for now ... === added directory 'dhis-2/dhis-web/dhis-web-ohie/src/main/java/org/hisp/dhis/web/ohie/adx' === added directory 'dhis-2/dhis-web/dhis-web-ohie/src/main/java/org/hisp/dhis/web/ohie/adx/webapi' === added file 'dhis-2/dhis-web/dhis-web-ohie/src/main/java/org/hisp/dhis/web/ohie/adx/webapi/ADXController.java' --- dhis-2/dhis-web/dhis-web-ohie/src/main/java/org/hisp/dhis/web/ohie/adx/webapi/ADXController.java 1970-01-01 00:00:00 +0000 +++ dhis-2/dhis-web/dhis-web-ohie/src/main/java/org/hisp/dhis/web/ohie/adx/webapi/ADXController.java 2015-07-03 16:31:04 +0000 @@ -0,0 +1,79 @@ +package org.hisp.dhis.web.ohie.adx.webapi; + +/* + * Copyright (c) 2004-2015, University of Oslo + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * Neither the name of the HISP project nor the names of its contributors may + * be used to endorse or promote products derived from this software without + * specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hisp.dhis.dxf2.common.ImportOptions; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import org.hisp.dhis.dxf2.adx.ADXDataService; +import org.hisp.dhis.dxf2.common.JacksonUtils; +import org.hisp.dhis.dxf2.importsummary.ImportSummaries; + +import static org.hisp.dhis.webapi.utils.ContextUtils.*; + +/** + * + * @author bobj + */ + +@Controller +@RequestMapping( value = ADXController.RESOURCE_PATH ) +public class ADXController +{ + public static final String RESOURCE_PATH = "/dataValueSets"; + + private static final Log log = LogFactory.getLog( ADXController.class ); + + @Autowired + private ADXDataService adxService; + + @RequestMapping( method = RequestMethod.POST, consumes = "application/xml" ) + @PreAuthorize( "hasRole('ALL') or hasRole('F_DATAVALUE_ADD')" ) + public void postXMLDataValueSet( ImportOptions importOptions, + HttpServletResponse response, InputStream in, Model model ) throws IOException + { + ImportSummaries importSummaries = adxService.postData( in, importOptions ); + + log.debug( "Data values set saved" ); + + response.setContentType( CONTENT_TYPE_XML ); + JacksonUtils.toXml( response.getOutputStream(), importSummaries ); + } + +} === modified file 'dhis-2/dhis-web/dhis-web-ohie/src/main/resources/META-INF/dhis/webapi-ohie.xml' --- dhis-2/dhis-web/dhis-web-ohie/src/main/resources/META-INF/dhis/webapi-ohie.xml 2015-05-11 07:10:34 +0000 +++ dhis-2/dhis-web/dhis-web-ohie/src/main/resources/META-INF/dhis/webapi-ohie.xml 2015-07-03 16:31:04 +0000 @@ -16,6 +16,8 @@ + + @@ -100,5 +102,5 @@ - +