Comprehensive guide for developers working on the UgandaEMR Sync Module.
- Development Environment Setup
- Building the Module
- Code Organization
- Adding New Features
- Testing
- Debugging
- Code Style Guidelines
- Contributing
- Java Development Kit (JDK): 8 or higher
- Maven: 3.6+
- Git: For version control
- IDE: IntelliJ IDEA (recommended) or Eclipse
- OpenMRS SDK: For local development
- Database: MySQL 5.7+ or PostgreSQL 9.6+
# Clone the repository
git clone https://github.com/METS-Programme/openmrs-module-ugandaemr-sync.git
cd openmrs-module-ugandaemr-sync
# Build the project
mvn clean install
# Skip tests (for faster builds during development)
mvn clean install -DskipTests# Install OpenMRS SDK
curl -s https://raw.githubusercontent.com/openmrs/openmrs-sdk/master/bin/install | bash
# Create new OpenMRS project
sdk create-project openmrs.openmrs-reference-module-2.9.0 mydevserver
# Add sync module to project
cd mydevserver
echo "openmrs.module.ugandaemrsync.enabled=true" >> openmrs-run.properties# Download OpenMRS standalone
wget https://sourceforge.net/projects/openmrs/files/releases/OpenMRS_Platform/2.7.0/openmrs-2.7.0.war
# Deploy to servlet container (Tomcat)
cp openmrs-module-ugandaemr-sync-2.0.6-SNAPSHOT.oam $TOMCAT_HOME/webapps/openmrs/WEB-INF/bundles/
# Start Tomcat
cd $TOMCAT_HOME/bin
./startup.sh- Import Project: File → Open → Select
pom.xml - Enable Annotation Processing:
- Settings → Build, Execution, Deployment → Compiler → Annotation Processors
- Enable "Enable annotation processing"
- Configure Code Style: Import Java code style from OpenMRS
- Set Up Database: Configure database connection in Data Sources
- Import as Maven Project: File → Import → Maven → Existing Maven Projects
- Enable Project Facets: Right-click project → Properties → Project Facets
- Configure Server: Add Tomcat server in Server view
# Clean and build with tests
mvn clean install
# Build without tests
mvn clean install -DskipTests
# Build specific module
cd api
mvn clean install# Build with specific profile
mvn clean install -Pdevelopment
# Build with debug information
mvn clean install -Dmaven.compiler.debug=true
# Build with custom OpenMRS version
mvn clean install -Dopenmrs.version=2.7.0openmrs-module-ugandaemr-sync/
├── api/ # API Module (business logic)
│ ├── src/main/java/
│ │ └── org/openmrs/module/ugandaemrsync/
│ │ ├── api/ # Service layer
│ │ ├── model/ # Domain models
│ │ ├── dao/ # Data access layer
│ │ ├── tasks/ # Scheduled tasks
│ │ ├── server/ # FHIR processing
│ │ ├── security/ # Security components
│ │ ├── circuitbreaker/ # Circuit breaker pattern
│ │ ├── exception/ # Custom exceptions
│ │ └── util/ # Utilities
│ └── src/main/resources/
│ ├── liquibase/ # Database migrations
│ └── scripts/ # SQL scripts
├── omod/ # OSGi Module (web/rest)
│ ├── src/main/java/
│ │ └── org/openmrs/module/ugandaemrsync/
│ │ ├── web/ # Web controllers
│ │ └── resource/ # REST resources
│ └── src/main/resources/
│ ├── config.xml # Module configuration
│ └── messages.properties # UI messages
└── pom.xml # Parent POM
Contains business logic and service interfaces
Contains REST resources and web controllers
Contains domain models and database entities
Contains data access objects and database queries
Contains scheduled task implementations
Contains FHIR processing logic
Contains security and authentication components
package org.openmrs.module.ugandaemrsync.tasks;
import org.openmrs.scheduler.tasks.AbstractTask;
import org.openmrs.api.context.Context;
import org.openmrs.module.ugandaemrsync.api.UgandaEMRSyncService;
public class MyCustomSyncTask extends AbstractTask {
@Override
public void execute() {
try {
UgandaEMRSyncService service = Context.getService(UgandaEMRSyncService.class);
// Your custom logic here
log.info("Executing custom sync task");
// Example: Process specific data
processCustomData(service);
} catch (Exception e) {
log.error("Error in custom sync task", e);
throw new RuntimeException("Custom sync task failed", e);
}
}
private void processCustomData(UgandaEMRSyncService service) {
// Implementation here
}
}package org.openmrs.module.ugandaemrsync.web.resource;
import org.openmrs.module.webservices.rest.web.resource.impl.DelegatingCrudResource;
import org.openmrs.module.webservices.rest.web.annotation.Resource;
@Resource(name = RestConstants.VERSION_1 + "/mycustomresource",
supportedClass = MyCustomModel.class)
public class MyCustomResource extends DelegatingCrudResource<MyCustomModel> {
@Override
public MyCustomModel newDelegate() {
return new MyCustomModel();
}
@Override
public MyCustomModel save(MyCustomModel delegate) {
return Context.getService(UgandaEMRSyncService.class)
.saveMyCustomModel(delegate);
}
@Override
public MyCustomModel getByUniqueId(String uniqueId) {
return Context.getService(UgandaEMRSyncService.class)
.getMyCustomModelByUuid(uniqueId);
}
// Implement other required methods...
}<!-- liquibase/changelog/my-custom-changes.xml -->
<changeSet id="add-custom-table" author="developer">
<createTable tableName="my_custom_table">
<column name="id" type="INT" autoIncrement="true">
<constraints primaryKey="true"/>
</column>
<column name="uuid" type="VARCHAR(38)">
<constraints unique="true"/>
</column>
<column name="name" type="VARCHAR(255)"/>
<column name="description" type="TEXT"/>
<column name="date_created" type="DATETIME"/>
</createTable>
</changeSet>public class UgandaEMRSyncServiceTest extends BaseModuleContextSensitiveTest {
@Autowired
private UgandaEMRSyncService service;
@Test
public void testSaveSyncFhirProfile() {
// Create test profile
SyncFhirProfile profile = new SyncFhirProfile();
profile.setName("Test Profile");
profile.setProfileEnabled(true);
// Save
SyncFhirProfile saved = service.saveSyncFhirProfile(profile);
// Verify
assertNotNull(saved.getSyncFhirProfileId());
assertEquals("Test Profile", saved.getName());
}
}@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:/applicationContext-service.xml"})
public class FhirIntegrationTest {
@Autowired
private UgandaEMRSyncService service;
@Test
public void testEndToEndFhirGeneration() {
// Create profile
SyncFhirProfile profile = createTestProfile();
// Generate FHIR resources
SyncFHIRRecord processor = new SyncFHIRRecord();
Collection<String> bundles = processor.generateFHIRResourceBundles(profile);
// Verify FHIR output
assertFalse(bundles.isEmpty());
// Validate FHIR format
for (String bundle : bundles) {
IBaseResource resource = FhirContext.forR4().newJsonParser().parseResource(bundle);
assertTrue(resource instanceof Bundle);
}
}
}# Run all tests
mvn test
# Run specific test class
mvn test -Dtest=UgandaEMRSyncServiceTest
# Run specific test method
mvn test -Dtest=UgandaEMRSyncServiceTest#testSaveSyncFhirProfile
# Run integration tests
mvn verify<!-- Add to OpenMRS log4j.xml -->
<logger name="org.openmrs.module.ugandaemrsync">
<level value="DEBUG"/>
</logger>
<logger name="org.openmrs.module.ugandaemrsync.server.SyncFHIRRecord">
<level value="TRACE"/>
</logger># Start OpenMRS with remote debugging enabled
export JAVA_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,address=5005,server=y,suspend=n"
catalina.sh run
# Connect from IDE
# IntelliJ: Run → Edit Configurations → Remote → Debug// Enable Hibernate SQL logging
log.info("Executing query: {}", sql);
// Log query parameters
log.info("Query parameters: {}", params);
// Log result count
log.info("Query returned {} results", results.size());-
Naming Conventions
- Classes: PascalCase (
SyncFhirProfile) - Methods: camelCase (
generateFHIRResources) - Constants: UPPER_SNAKE_CASE (
CONNECTION_SUCCESS_200) - Variables: camelCase (
patientList)
- Classes: PascalCase (
-
Exception Handling
// GOOD: Specific exceptions try { processFhirData(); } catch (FHIRValidationException e) { log.error("FHIR validation failed", e); throw new UgandaEMRSyncException("Invalid FHIR data", e); }
-
Documentation
/** * Generate FHIR resources for a specific profile. * * @param profile the FHIR profile configuration * @return collection of FHIR resource bundles * @throws UgandaEMRSyncException if FHIR generation fails */ public Collection<String> generateFHIRResources(SyncFhirProfile profile) { // Implementation }
-
Use Parameterized Queries
-- GOOD SELECT * FROM patient WHERE patient_id = :patientId -- BAD SELECT * FROM patient WHERE patient_id = 123
-
Naming Conventions
-- Tables: snake_case sync_fhir_profile, sync_fhir_resource -- Columns: snake_case profile_enabled, case_based_primary_resource_type
-
Fork the repository
- Create a fork on GitHub
- Clone your fork locally
-
Create a feature branch
git checkout -b feature/my-feature
-
Make changes
- Write code following style guidelines
- Add tests for new functionality
- Update documentation
-
Test changes
mvn clean test -
Commit changes
git add . git commit -m "Add feature: description of changes"
-
Push to fork
git push origin feature/my-feature
-
Create Pull Request
- Go to GitHub
- Create pull request
- Provide clear description of changes
- Title: Clear, concise description
- Description: Detailed explanation of changes
- Testing: Describe testing performed
- Documentation: Update relevant docs
- Breaking Changes: Clearly indicate if present
- Automated checks (CI/CD)
- Peer review
- Address feedback
- Approval and merge
- Use semantic versioning:
MAJOR.MINOR.PATCH - Example:
2.0.6-SNAPSHOT→2.0.6→2.0.7-SNAPSHOT
-
Update Version Numbers
<!-- pom.xml --> <version>2.0.6</version> <!-- Remove -SNAPSHOT -->
-
Update Documentation
- Update README.md with new features
- Update CHANGELOG.md
- Update API documentation
-
Run Full Test Suite
mvn clean test -
Create Release Tag
git tag -a v2.0.6 -m "Release version 2.0.6" git push origin v2.0.6 -
Build Release Artifacts
mvn clean deploy
-
Publish Release
- Upload to OpenMRS modules repository
- Create GitHub release
- Announce on community forums
Last Updated: May 2, 2026
Maintained By: METS Programme Development Team