There are so many ways you can integrate LDAP with Liferay that I think a whole book could be filled with examples. Clearly this is a consequence of the many different scenarios you might face in your company.
Showcase
The showcase I am presenting is:
1. ApacheDS hosts users and groups (roles and groups are the same on the LDAP side in other words we do not have the diferentiation Liferay does have)
2. ApacheDS is accessible from spring security
3. ApacheDS is accessible from Liferay 5.2.3 LDAP implementation following the below user stories.
I have documented already the ApacheDS setup. Of you have not setup ApacheDS read here.
I have also documented on the same link how to get Spring security working with ApacheDS.
User Stories
Liferay LDAP authentication user stories:
1. When a user is set in LDAP, Then the user can login with his credentials when accessing Liferay even if the user has never been set in Liferay
2. When a user is assigned to a group/role in LDAP, Then after user login the new group and the user-group association will be created in Liferay
3. When a user is detached from a group/role in LDAP, Then after user login the user-group association will be removed.
The above guarrantees that we can handle the setup of users for both applications (Portlets and Servlets applications) in just one LDAP server.
Implementation
I thought this was going to be an easy plumbing but it turned to be not that easy. I posted the issue and continued investigating to arrive to the following solution.
1. Spring Security will work only if group contains users (uniqueMember attribute in the group cn)
2. Liferay can work as expected only if user contains groups (any attribute that points to a valid group cn)
3. I have added then an extra attribute to users (ou) which basically closes a cyclic reference between users and groups.
4. See below for the configuration in liferay. Note that I do not include the groups section as in Liferay you must decide either to import users or groups. If you import groups you will not be able to login in version 5.2.3 as I posted in the issues link. I do not show the import/export section as I do not import nor export users and roles. As said before this showcase is precisely about leaving those tasks to LDAP alone. Performance wise this is a good decision BTW.
5. Of course we need to build an application that handles this cyclical reference.
Thursday, September 30, 2010
Tuesday, September 28, 2010
ETL: Importing data with Talend
ETL is used for Operational Data Stores, Data Warehouse and Data Mart. ETL tools can also be handy for simple importing into existing application databases as well.
Importing data into your application is as important as reporting out of your application. Importing can be achieved while distributing the workload through several people.
On one end you want a BA to decide when to run an import process, which components to assembly, which data sources to use, the mappings.
There is an existing model, datasources representing feeds that need to get to your model and a mapping to make that transformation happen. ETL processes are commonly used for this task. You can do something as simple as manual SQL scripting, something more elaborated using a Rules Engine or even more polished like using an ETL tool.
Talend has a very good tool for ETL (which JasperETL uses as well). The fact that latest JasperETL 3.2.3 does not work that well on MAC OSX even after tweaking made me decide to stick to Talend. These tools can be used for:
This tutorial is about designing a Job with Talend/Jasper ETL. I am not interested here in covering points 1 and 3 as they are really not needed for our task: Importing data from Excel into an application database.
Download Talend ( I used TOS-All-r48998-V4.1.0RC3.zip ) and uncompress in c:\TOS-All-r48998-V4.1.0RC3
Let us review in detail what I have done in this simple project.
1. To export: Right click on “Business Models” and export “all” to the root folder (in our case C:\projects\) that is shared let us say on a subversion repository. This will create/update “c:\projects\DbImport”. Now you can share that on SVN.
2. To import: Checkout from SVN. Go to Talend and import.
As a better option you can (at least in Talend version 4.1.0) share the whole project (which does not include any binaries) Below is the list of all files for the project in this tutorial:
Unfortunately SVN support is not included in the IDE. Following some steps though you can still share the project.
Importing data into your application is as important as reporting out of your application. Importing can be achieved while distributing the workload through several people.
On one end you want a BA to decide when to run an import process, which components to assembly, which data sources to use, the mappings.
There is an existing model, datasources representing feeds that need to get to your model and a mapping to make that transformation happen. ETL processes are commonly used for this task. You can do something as simple as manual SQL scripting, something more elaborated using a Rules Engine or even more polished like using an ETL tool.
Talend has a very good tool for ETL (which JasperETL uses as well). The fact that latest JasperETL 3.2.3 does not work that well on MAC OSX even after tweaking made me decide to stick to Talend. These tools can be used for:
- Construct a Business model with a graphical interface that allows BA to drop the general blocks for example an Excel source file to be used in conjunction with a web service output to fill out records in an existing database.
- Design a Job to implement the business model blocks.
- Schedule Jobs.
This tutorial is about designing a Job with Talend/Jasper ETL. I am not interested here in covering points 1 and 3 as they are really not needed for our task: Importing data from Excel into an application database.
For the impatient
Create a local MySQL DB and name it myapp. Use the model from http://thinkinginsoftware.blogspot.com/2010/09/jasper-real-time-report-services.htmlDownload Talend ( I used TOS-All-r48998-V4.1.0RC3.zip ) and uncompress in c:\TOS-All-r48998-V4.1.0RC3
- Create folder c:\projects\talend to be used as workspace
- Open Talend (use the non-wpf exe file for example TalendOpenStudio-win32-x86.exe ) and create a project named DbImport. Use as workspace folder c:\projects\talend
- After Talend is done updating the project close it.
- Checkout http://nestorurquiza.googlecode.com/svn/trunk/talend/DbImport/ in a temporary directory
- Copy all files from the temporary directory to the workspace DbImport folder
- I have included a sample Excel employees.xls with the project. Create folder c:\projects\DbImport and drop the excel there
- Start Talend and open project DbImport
- Run PopulateAll job and confirm offices, departments and employees have been added to “myapp” database
- Note: Even if no changes are made after closing the IDE you will get differences in some projects files. It is good idea to always update from svn before starting to work on a project as other developers might commit their local project files to the repository.
Let us review in detail what I have done in this simple project.
Showcase
- Go ahead and create a mySQL database named “myapp”. We are going to use the simple model we created on http://thinkinginsoftware.blogspot.com/2010/09/jasper-real-time-report-services.html
- We want to import employees from an existing Excel spreadsheet into our new database.
- Our Excel import file contains a de-normalized data we need to import into our normalized tables. The columns are:
first_name last_name office_name department_name
Using Talend / Jasper ETL
- Install Talend (I am using version 4.1.0) or Jasper ETL (I have tested version 3.2.3 which at least in MAC OSX needs a little tweak). Install it near the File System root. I will describe here everything for Windows OS but you should be able to to the same in other OS. The important is to keep paths reusable through your team. So install Talend then in “c:\talend”
- Start the program (If using Windows XP use the executable, for example TalendOpenStudio-win32-x86.exe. The Eclipse -wpf- version flicks).Set up a connection. Click on the button near “Repository” (In Talend is “Email” button), provide your email (this will end up stamped in many files from now on so use a real personal or work email). For the workspace folder use a common folder that other users can later use as well in their own machines for example “C:\projects\talend”. You will need to hit “restart” if using Talend and changing the workspace.
- Select “Create a new Local project” from “Project” section and click on “Go”. Use as project name “DbImport” and as language generation “Java”. Pick the new project from the last dropdown. Click on “Open”.
- After you close the welcome windows you should see the “Window|Perspective|Design Workspace”. Right click on Job Designs on the left and create the first Job called “PopulateLookup” with Purpose “Import from Excel to lookup MySQL tables”. This is a job that will populate department and office tables. We need office_id and department_id for the employee table, that is why we must be sure the department and office exist in the DB before.
- On the repository View (left panel by default) right click on Metadata/Db Connection and create a MySQL connection to the database containing the tables employee, department and office tables. Right click on the connection and “retrieve schema” for the three tables.
- Create an Excel file named c:\projects\DbImport\employees.xls containing the data in the appendix.
- Right click on Metadata/File Excel and point to a local Excel file (Use Path button to point to the file)
- Select the sheet, click next, select “Set heading row as column names”, click next and select as name “employee”
- Drag and drop one by one (into the job area) the department and office metadata (when prompted select tMySQLOutput)
- Look for the Palette (Components Library). If it is not showing up use “Window|Show View|General|Palette.”
- Drag and drop a tMap component from the palette “Processing” section.
- Right click on the employee inner square (you must select the inner square otherwise the option will not be available) and select “Row|Main” a line will be started and will end wherever you click as final destination component. In this case click on the tMap component.
- Right click on tMap, select “Row|New Output” and drop the line into department and name it outputDepartment. Do the same for Office. When prompted to get the schema from the target respond yes as that helps to see the available destination fields.
- Double click on the tMap and drag and drop the fields from the input to the output
- Run the Job from the Run tab. If there are problems the specific component will be red and double clicking on it will show up a description of the problem. You might notice that is the situation as we have specified office_name instead of just name as the destination field in the tMap, so correct that and rerun.
- Save your job and create a second job named “PopulateEmployee” purpose “Populate table employee”
- Drop department and office boxes into the working area. Be sure to select type tMySQLInput
- Drop the employee Excel
- Drop a tMap
- Drop the employee MySQL as tMySQLOutput
- Create input and output connections as explained before. Use naming conventions for example inputEmployee, inputOffice, inputDepartment and outputEmployee
- Open the tMap and in the input panel drag and drop the inputOffice.name to inputEmployee.office_name and inputDepartment.name to inputEmployee.department_name. Here you are defining the necessary joins from input sources.
- Drag and drop inputOffice.id, inputDepartment.id, inputEmployee.first_name and inputEmployee.last_name into the output panel left colum right next to the destination field.
- Run the project to get the data imported. Check the data from the mySQL tables
- Of course both jobs are related. We want to run PopulateLookup and then later PopulateEmployee. That is why we need to create a third job now. Name it “PopulateAll”
- Drop two components type “tRunJob” from the palette. From the Component tab select for the first “PopulateLookup” and for the second “PopulateEmployee”
- Right click on the first and select “Row|Main”. Drop the line into the second sub job.
- Cleanup the records from the database so you can see all recreated.
delete from office; delete from employee; delete from office;
- Run “populateAll” job and your data will be in the destination.
Sharing the project
Talend and so JasperETL are designed in a way that they have version control through a server. To avoid using an extra sever you could use export/import (but that would be limiting):1. To export: Right click on “Business Models” and export “all” to the root folder (in our case C:\projects\) that is shared let us say on a subversion repository. This will create/update “c:\projects\DbImport”. Now you can share that on SVN.
2. To import: Checkout from SVN. Go to Talend and import.
As a better option you can (at least in Talend version 4.1.0) share the whole project (which does not include any binaries) Below is the list of all files for the project in this tutorial:
|-- TDQ_Data Profiling | |-- Analyses | `-- Reports |-- TDQ_Libraries | |-- Indicators | |-- JRXML Template | |-- Patterns | `-- Rules |-- businessProcess |-- businessProcessSVG |-- code | |-- jobscripts | |-- routines | | `-- system | | |-- DataOperation_0.1.item | | |-- DataOperation_0.1.properties | | |-- Mathematical_0.1.item | | |-- Mathematical_0.1.properties | | |-- Numeric_0.1.item | | |-- Numeric_0.1.properties | | |-- Relational_0.1.item | | |-- Relational_0.1.properties | | |-- StringHandling_0.1.item | | |-- StringHandling_0.1.properties | | |-- TalendDataGenerator_0.1.item | | |-- TalendDataGenerator_0.1.properties | | |-- TalendDate_0.1.item | | |-- TalendDate_0.1.properties | | |-- TalendString_0.1.item | | `-- TalendString_0.1.properties | `-- snippets |-- components |-- context |-- documentations |-- images | |-- job_outlines | `-- joblet_outlines |-- joblets |-- libs |-- metadata | |-- BRMSconnections | |-- FTPconnections | |-- LDAPSchema | |-- MDMconnections | |-- SalesforceSchema | |-- WSDLSchema | |-- connections | | |-- myapp_0.1.item | | `-- myapp_0.1.properties | |-- fileDelimited | |-- fileEBCDIC | |-- fileExcel | | |-- employee_0.1.item | | `-- employee_0.1.properties | |-- fileHL7 | |-- fileLdif | |-- filePositional | |-- fileRegex | |-- fileXml | |-- genericSchema | |-- header_footer | |-- rules | `-- sapconnections |-- process | |-- PopulateEmployee_0.1.item | |-- PopulateEmployee_0.1.properties | |-- PopulateLookup_0.1.item | |-- PopulateLookup_0.1.properties | |-- populateAll_0.1.item | `-- populateAll_0.1.properties |-- sqlPatterns | |-- Generic | | |-- UserDefined | | `-- system | | |-- Aggregate_0.1.item | | |-- Aggregate_0.1.properties | | |-- Commit_0.1.item | | |-- Commit_0.1.properties | | |-- DropSourceTable_0.1.item | | |-- DropSourceTable_0.1.properties | | |-- DropTargetTable_0.1.item | | |-- DropTargetTable_0.1.properties | | |-- FilterColumns_0.1.item | | |-- FilterColumns_0.1.properties | | |-- FilterRow_0.1.item | | |-- FilterRow_0.1.properties | | |-- MergeInsert_0.1.item | | |-- MergeInsert_0.1.properties | | |-- MergeUpdate_0.1.item | | |-- MergeUpdate_0.1.properties | | |-- Rollback_0.1.item | | `-- Rollback_0.1.properties | |-- Hive | | |-- UserDefined | | `-- system | | |-- HiveAggregate_0.1.item | | |-- HiveAggregate_0.1.properties | | |-- HiveCreateSourceTable_0.1.item | | |-- HiveCreateSourceTable_0.1.properties | | |-- HiveCreateTargetTable_0.1.item | | |-- HiveCreateTargetTable_0.1.properties | | |-- HiveDropSourceTable_0.1.item | | |-- HiveDropSourceTable_0.1.properties | | |-- HiveDropTargetTable_0.1.item | | |-- HiveDropTargetTable_0.1.properties | | |-- HiveFilterColumns_0.1.item | | |-- HiveFilterColumns_0.1.properties | | |-- HiveFilterRow_0.1.item | | `-- HiveFilterRow_0.1.properties | |-- MySQL | | |-- UserDefined | | `-- system | | |-- MySQLAggregate_0.1.item | | |-- MySQLAggregate_0.1.properties | | |-- MySQLCreateSourceTable_0.1.item | | |-- MySQLCreateSourceTable_0.1.properties | | |-- MySQLCreateTargetTable_0.1.item | | |-- MySQLCreateTargetTable_0.1.properties | | |-- MySQLDropSourceTable_0.1.item | | |-- MySQLDropSourceTable_0.1.properties | | |-- MySQLDropTargetTable_0.1.item | | |-- MySQLDropTargetTable_0.1.properties | | |-- MySQLFilterColumns_0.1.item | | |-- MySQLFilterColumns_0.1.properties | | |-- MySQLFilterRow_0.1.item | | `-- MySQLFilterRow_0.1.properties | |-- Netezza | | |-- UserDefined | | `-- system | | |-- NetezzaAggregate_0.1.item | | |-- NetezzaAggregate_0.1.properties | | |-- NetezzaCreateSourceTable_0.1.item | | |-- NetezzaCreateSourceTable_0.1.properties | | |-- NetezzaCreateTargetTable_0.1.item | | |-- NetezzaCreateTargetTable_0.1.properties | | |-- NetezzaDropSourceTable_0.1.item | | |-- NetezzaDropSourceTable_0.1.properties | | |-- NetezzaDropTargetTable_0.1.item | | |-- NetezzaDropTargetTable_0.1.properties | | |-- NetezzaFilterColumns_0.1.item | | |-- NetezzaFilterColumns_0.1.properties | | |-- NetezzaFilterRow_0.1.item | | `-- NetezzaFilterRow_0.1.properties | |-- Oracle | | |-- UserDefined | | `-- system | | |-- OracleAggregate_0.1.item | | |-- OracleAggregate_0.1.properties | | |-- OracleCreateSourceTable_0.1.item | | |-- OracleCreateSourceTable_0.1.properties | | |-- OracleCreateTargetTable_0.1.item | | |-- OracleCreateTargetTable_0.1.properties | | |-- OracleDropSourceTable_0.1.item | | |-- OracleDropSourceTable_0.1.properties | | |-- OracleDropTargetTable_0.1.item | | |-- OracleDropTargetTable_0.1.properties | | |-- OracleFilterColumns_0.1.item | | |-- OracleFilterColumns_0.1.properties | | |-- OracleFilterRow_0.1.item | | |-- OracleFilterRow_0.1.properties | | |-- OracleMerge_0.1.item | | `-- OracleMerge_0.1.properties | |-- ParAccel | | |-- UserDefined | | `-- system | | |-- ParAccelAggregate_0.1.item | | |-- ParAccelAggregate_0.1.properties | | |-- ParAccelCommit_0.1.item | | |-- ParAccelCommit_0.1.properties | | |-- ParAccelDropSourceTable_0.1.item | | |-- ParAccelDropSourceTable_0.1.properties | | |-- ParAccelDropTargetTable_0.1.item | | |-- ParAccelDropTargetTable_0.1.properties | | |-- ParAccelFilterColumns_0.1.item | | |-- ParAccelFilterColumns_0.1.properties | | |-- ParAccelFilterRow_0.1.item | | |-- ParAccelFilterRow_0.1.properties | | |-- ParAccelRollback_0.1.item | | `-- ParAccelRollback_0.1.properties | `-- Teradata | |-- UserDefined | `-- system | |-- TeradataAggregate_0.1.item | |-- TeradataAggregate_0.1.properties | |-- TeradataColumnList_0.1.item | |-- TeradataColumnList_0.1.properties | |-- TeradataCreateSourceTable_0.1.item | |-- TeradataCreateSourceTable_0.1.properties | |-- TeradataCreateTargetTable_0.1.item | |-- TeradataCreateTargetTable_0.1.properties | |-- TeradataDropSourceTable_0.1.item | |-- TeradataDropSourceTable_0.1.properties | |-- TeradataDropTargetTable_0.1.item | |-- TeradataDropTargetTable_0.1.properties | |-- TeradataFilterColumns_0.1.item | |-- TeradataFilterColumns_0.1.properties | |-- TeradataFilterRow_0.1.item | |-- TeradataFilterRow_0.1.properties | |-- TeradataTableList_0.1.item | `-- TeradataTableList_0.1.properties |-- talend.project `-- temp
Unfortunately SVN support is not included in the IDE. Following some steps though you can still share the project.
Commit the project to SVN
- Create “DbImport” project as explained before.
- Delete temp directory
- Import in your SVN
- Checkout the project from SVN
- Add svn:ignore for the temp directory (svn propset svn:ignore "temp" .)
- Commit the project.
Check out the project from SVN
- Create a new local “DbImport” project. Close the IDE.
- Outside the workspace folder checkout “DbImport” from SVN.
- Replace the content of the workspace “DbImport” directory with the checked from SVN files.
- Open the IDE and modify the project as you wish.
- Close the IDE and use svn update and/or commit commands as you need.
Documentation
- http://sourceforge.net/projects/jasperetl/files/
- http://talend.dreamhosters.com/tos/user-guide-download/V402/DocumentationSet_UG&RG_40b_EN.zip
- Help from the GUI
Appendix
| first_name | last_name | office_name | department_name |
| John | Smith | London | Legal |
| Mathew | Parker | USA | Marketing |
| Andrea | Polini | Rome | Sales |
Friday, September 24, 2010
Android YNotifier: A Yahoo Email Notifier
Yahoo Email Notifier (YNotifier) allows you to configure a yahoo id (please note you must provide id and not the complete email address) and password to get notified about new and unread emails.
* It checks Yahoo every 5 minutes.
* Just click the alert and land in yahoo mobile to check, reply or send new emails.
* Once installed it will start automatically every time you restart your phone.
* To stop checking emails just leave id and password empty.
* It won’t bother you with the same unread emails alert if you do not perform any actions after clicking the alert and no new emails are received.
* For support go to http://thinkinginsoftware.blogspot.com/ynotifier
* If the application does not work as expected please check you actually have unread emails in your yahoo mail from a Desktop/Laptop computer. Then be sure you have configured your correct user id and password.
* If you suspect YNotifier is responsible for any performance issues you can install "TaskPanel" and kill "YNotifier".
* If you find out any problems (bugs) please install "Log Collector" and send me the content by email.
* It checks Yahoo every 5 minutes.
* Just click the alert and land in yahoo mobile to check, reply or send new emails.
* Once installed it will start automatically every time you restart your phone.
* To stop checking emails just leave id and password empty.
* It won’t bother you with the same unread emails alert if you do not perform any actions after clicking the alert and no new emails are received.
* For support go to http://thinkinginsoftware.blogspot.com/ynotifier
Installation
Go to Google market and download Ynotifier or if navigating this page from your android device click here to get any of my applicationsSupport
Use this page for support, questions, enhancements and feature requests. Post any issues here or drop me an email. I will be glad to help making this application better.* If the application does not work as expected please check you actually have unread emails in your yahoo mail from a Desktop/Laptop computer. Then be sure you have configured your correct user id and password.
* If you suspect YNotifier is responsible for any performance issues you can install "TaskPanel" and kill "YNotifier".
* If you find out any problems (bugs) please install "Log Collector" and send me the content by email.
Note for IPhone customers
If there is enough demand I will make it available to the IPhone community as well. So drop me an email if interested.Real Time Database Documentation
All the information about a project should be maintained from within the project whenever is possible. I have never seen a perfect documentation nor an updated documentation to live long enogh in any company I have been as employee or contractor. That is why I believe that the most valuable documentation is the one that can be auto generated when you need it.
The database or Model information in particular is very important as that is the bottom layer, the foundation of any modern software architecture. Having the ERD/EER available is then a must have for the agility of a true Business Driven development (BDD). Enough to say it allows to plan for new features providing just the gap to be implemented.
1. Provide business access to your current database metadata. You are maintaining it in a SCM repository, aren't you? This is the best way for business to see your naming conventions are aligned with their business language. So let them see your current tables and fields.
2. Provide business with all current default values. I hope you understand you must script them and keep them in a repository as well.
3. Provide business with a tool to get the EER/ERD by themselves. For MySQL install MySQL Workbench. Here is all they need to get a whole diagram whenever they want:
1. Open a MySQL Workbench already saved project. In a project some of the settings will be already saved but youy are free to create one project from scratch every time you want a new EER/ERD.
2. Choose "Create EER Model From SQL Script" from the Home page and point to the metadata file.
3. Select "Model|Create Diagram From Catalog Objects" to generate the diagram.
4. If the tables show up too tight select "Model|Diagram Properties and size" and expand columns and rows as needed.
5. Hit Arrange/Autolayout. If you need more space go back to the previous step.
6. In big diagrams sometimes we do not need all information about foreihn keys for example. From preferences/diagram select “Hide Captions” and deselect “Draw Line crossings” and “Center captions over the line”
7. Confirm all tables are viewable and save the project.
Here is a nice command to cleanup the MySQL database metadata (also referred as dump file) to show just tables and fields. You can encapsulate it in a batch/bash script so business guys have a cleaner file to look into when wondering if a given keyword has been already used in the system.
The database or Model information in particular is very important as that is the bottom layer, the foundation of any modern software architecture. Having the ERD/EER available is then a must have for the agility of a true Business Driven development (BDD). Enough to say it allows to plan for new features providing just the gap to be implemented.
1. Provide business access to your current database metadata. You are maintaining it in a SCM repository, aren't you? This is the best way for business to see your naming conventions are aligned with their business language. So let them see your current tables and fields.
2. Provide business with all current default values. I hope you understand you must script them and keep them in a repository as well.
3. Provide business with a tool to get the EER/ERD by themselves. For MySQL install MySQL Workbench. Here is all they need to get a whole diagram whenever they want:
1. Open a MySQL Workbench already saved project. In a project some of the settings will be already saved but youy are free to create one project from scratch every time you want a new EER/ERD.
2. Choose "Create EER Model From SQL Script" from the Home page and point to the metadata file.
3. Select "Model|Create Diagram From Catalog Objects" to generate the diagram.
4. If the tables show up too tight select "Model|Diagram Properties and size" and expand columns and rows as needed.
5. Hit Arrange/Autolayout. If you need more space go back to the previous step.
6. In big diagrams sometimes we do not need all information about foreihn keys for example. From preferences/diagram select “Hide Captions” and deselect “Draw Line crossings” and “Center captions over the line”
7. Confirm all tables are viewable and save the project.
Here is a nice command to cleanup the MySQL database metadata (also referred as dump file) to show just tables and fields. You can encapsulate it in a batch/bash script so business guys have a cleaner file to look into when wondering if a given keyword has been already used in the system.
cat application.sql |grep -v "^/.*"|grep -v "\-.*"|grep -v "DROP.*"|grep -v "\`id.*"|grep -v "\`version.*"|grep -v "UNIQUE.*"|grep -v "KEY.*"|sed s/ENGINE.*//g|sed s/CREATE.//g > application_tables_and_fields.txt
Wednesday, September 22, 2010
Jasper Real Time Report Services Framework
Operational reporting (real time reporting) is an important part of the company software. The cost involved on Real Time Reporting can be high as it affects the existing system transactional capabilities.
On the other hand client reporting is usually done with data stored in a data warehouse. The data in there has certain delay and gets populated sometimes even just daily.
So the first task when designing reports is identifying which data can simply be refreshed from time to time and which one must be done in real time.
Once we have identified the data sets that must be generated in real time then we run into a new issue, some data must be joined but they are in a non linkable sources or even worst some of the data to be joined comes from an external application for example a Web Service. In those cases you will need a custom reporting solution.
A good custom reporting solution must provide the the best trade between high data availability for reporting and good application performance.
Resources are not unlimited and it is crucial that we use those we have at highest as possible percentage. The MVC pattern is to be applied to any software with a User Interface (UI) and I say UI and not GUI because even in the case of Console applications you still have a View.
If your needs are just pulling information from one existing database and you can live with just SQL then any Report Designer will be able to easily use any report utility like iReport to generate even the more complex reports you can imagine.
Of course real world is far from that. You need to pull data from different databases, some of them data warehouses and some of them real time application databases. You need data from other sources like Web Services, excel, text files and even (God forbid) pdf documents.
Only a high level language can come to your rescue to get Real Time Reporting in place.
Take a look at the below diagram
Regular users see a list of reports generated from files in the file system. The files follow a convention_name.jrxml. They are Jasper XML files. The user selects a report and a form shows up asking for parameters or if not parameters are needed a pdf will be returned with the contents of the report.
Report Designers use iReport tool. They build reports containing subreports. They use parameters to communicate from the main report to the subreport or to customize dynamically the necessary data sources connections. They use a connection to a local database that is built following the indications from a Java Developer (from files daily.sql and daily_data.sql for example). When they are satisfied with the result they copy reports and subreports in a specific file system path. For example rt_daily.jasper will be generated from rt_daily.jrxml. The jrxml is maintained in SCM of course.
Java Developers build code that (look at the numbers in the diagram):
1. Decides which connection to supply to the main report. The data source comes as part of the report name. For the example it is “rt” which means realtime and so a local sqlite database will be populated using the same metadata iReport designers used for their tests (daily.sql). It can be a connection to a non realtime database for example a CRM database. Regardless of what connection we supply the “realTimeDbPath” will be passed to Jasper in the case any subreport needs a realtime connection.
2. Runs a Service#populateDb() method following a convention like for example “DailyReportService”. This service is in charge of preparing the local sqlite with all needed datasets for either the main report or subreports.
3. Resolves the name of the compiled report file to pull from the file system (rt_daily.jasper)
4. Supplies connection, parameters and jasper file to the Jasper Report Engine to get a pdf file with the results of the report. All parameters supplied as part of the form are supplied to the Jasper Engine BTW. This allows to minimize coding but also imposes a security concern. Be sure you do not rely just on parameters but on internal security at services layer. Spring Security with the help of AOP is ideal for this.
Ideally someone that knows how the report should look will take care of this layer. This person just cares about the organization and layout of the data. One important assumption should be made at this point: The report writer shouldn’t necessarily be a DB developer, a High Level Language (HLL) developer (like Java, C# developers) or any other technical person. The report writter should have available certain datasources to visualize his report. The writer should be familiar with basic SQL concepts. The writter can be and should be IMO a Business Analyst (BA)
Model: Data
To get the report your company needs you will probably need to dig into Excel, SOAP services, text documents, databases, XML you name it. It makes sense though that this data gets translated to fixed tables from where the person in charge of the View could easily build the report. A DB developer is the best fit for this layer. A HLL programmer is a good fit as well. A BA can definitely build a reporting model as well. After all nobody better than him knows the dataset he will need per subreport.
Controller: Logic
An HLL developer will be needed for this layer. This is the layer in charge of all the plumbing between View and Model:
1. Uses a Services layer that in turn uses a DAO layer.
2. Implements security to determine which users have access to run which stored reports.
3. When the report is run it looks for the need of any real time data and if needed it populates it.
4. It invokes the Jasper Engine to run the particular report.
Let us say that our original DB has three tables
1. Open iReport. Go to preferences and be sure in classpath and point to sqlite3 driver file: sqlitejdbc-v056.jar. Of course you need to download that file if you do not have it. Just Google it.
2. Select File | New | Report | Blank | Launch Report Wizard. In Connections pick a new Database JDBC connection and use the below settings (You can always reconfigure this from the Designer toolbar clicking on the Report Datasources icon):
3. Hit Test button (username/password both are blank) and you should get a successful message. Hit Save. 4. In Query(SQL) paste this:
4. Expand “Fields” on the left and drag each of them to the Detail portion of the report.
5. From the reports element palette on the right drag labels to the Column Header section.
6. Click on Preview and the report should show the two records we added initially to the employee table. When you hit “Preview” Jasper compiles the .jrxml file into a .jasper file. This .jasper file will be deployed in a reports folder that the application reads so the expensive report compilation will be done by the iReport user and we will reuse it later from our backend.
1. Establish the dataset you will need and script metadata. and data. Here is sampleCheck.sql:
2. Script the data. Here is sampleCheck_data.sql
3. Build a local DB to be used to design the report:
4. At this point table “links” exists inside sampleCheck.db so open iReport and create a report called rt_sampleCheck. A file with extension .jrxml will be created. Use as data source the newly created DB. So:
5. Open iReport and include title and URL. Create a parameter called “fullName” Run the report. When asked for the parameter value use “Nestor Urquiza” The jasper file (rt_sampleCheck.jasper) is generated in the same directory where the jrxml file is.
6. Create the DAO implementation (SampleCheckReportDAO) that accepts a List of Objects to persist
7. Create a Service (SampleCheckReportService) that queries Bing for all users full names.
8. As we have follow conventions now it will be enough to call /report/rt/sampleCheck?fullName=Nestor+Urquiza from the browser. Note that we need a parameter this time. The parameter is inserted as is as a report param that is why the convention is so important here. Note also we could have live without a parameter in this simple example as we can restrict from java what we populate in the table however this is needed for our next final example. We will use sampleCheck as a subreport of sampleEmployee
The parent report must have empty path to look for the subreport. The reason is that we will drop all reports in the same folder to avoid another parameter to be passed (the subreport path)
All that is left now is to run our main report from Java. So we need to provide as a parameter “realTimeDbPath”. From Java we will need to populate of course the subreport data source and that is why we call the sampleCheckService from sampleEmployeeService. We can check still /report/rt/sampleCheck?fullName=Nestor+Urquiza works. Now /report/rt/sampleEmployee is rendering the sampleCheck subreport as well. Note that sampleCheck subreport gets the fullName from the master report so there is no need to insert it from Java. Still this works both from iDesigner with no Server in the middle (ideal for report writters) and from Java which is of course needed to present dynamic real time data. It would be great if JasperReports would allow for the use of connectionExpression element at the main report level and not only at subreport level. Unfortunately that is not the case and that is why we need to supply the main report connection as part of the URL (Jasper Report engine will not be able to dynamically based on a parameter discover which connection to use for the master report) So for the master report we provide a connection object whereas for the subreports we use “connectionExpression”. The expression will need to use the “realTimeDbPath” parameter when the report uses a real time data set or will be completely hardcoded when using any SQL database. Note that datasources do not necessarily have to be hardcoded in subreports “connectionExpression” elements. We can always use parameters to build them on the fly as already explained.
Below is a snapshot of the pdf report obtained from the web request:
And here a snapshot of the same from iReport. Same JRXML used from the backend and the frontend without stepping on each other toes.
1. script.sql
2. script_data.sql
3. Path where to put the compiled report file so it shows up from the web interface
4. Agree on a name for the report. Use Spring Resources for internationalization so when showing the name of the report it can show a more descriptive name but try hard to get a name that makes sense to everybody.
As a report Designer you will need to:
1. run the below commands:
2. Create a report and use local.db from above as data source. Naming conventions is important. The report must be named using the data source name followed by underscore and then the name of the service agreed with the developer. Case matters, so be aware.
3. Configure subreport "subreportParameter" and "connectionExpression" nodes. Here is a list of useful connection expressions: realtime sqlite:
4. Put the results in the server reports directory.
You can download the Jasper sources and sqlite databases from here.
On the other hand client reporting is usually done with data stored in a data warehouse. The data in there has certain delay and gets populated sometimes even just daily.
So the first task when designing reports is identifying which data can simply be refreshed from time to time and which one must be done in real time.
Once we have identified the data sets that must be generated in real time then we run into a new issue, some data must be joined but they are in a non linkable sources or even worst some of the data to be joined comes from an external application for example a Web Service. In those cases you will need a custom reporting solution.
A good custom reporting solution must provide the the best trade between high data availability for reporting and good application performance.
Resources are not unlimited and it is crucial that we use those we have at highest as possible percentage. The MVC pattern is to be applied to any software with a User Interface (UI) and I say UI and not GUI because even in the case of Console applications you still have a View.
If your needs are just pulling information from one existing database and you can live with just SQL then any Report Designer will be able to easily use any report utility like iReport to generate even the more complex reports you can imagine.
Of course real world is far from that. You need to pull data from different databases, some of them data warehouses and some of them real time application databases. You need data from other sources like Web Services, excel, text files and even (God forbid) pdf documents.
Only a high level language can come to your rescue to get Real Time Reporting in place.
The architecture
The purpose of this post is to document one implementation using Jasper Reports. I will show how real time reporting can be done while still separating the concerns of visualization, data and logic.Take a look at the below diagram
Regular users see a list of reports generated from files in the file system. The files follow a convention
Report Designers use iReport tool. They build reports containing subreports. They use parameters to communicate from the main report to the subreport or to customize dynamically the necessary data sources connections. They use a connection to a local database that is built following the indications from a Java Developer (from files daily.sql and daily_data.sql for example). When they are satisfied with the result they copy reports and subreports in a specific file system path. For example rt_daily.jasper will be generated from rt_daily.jrxml. The jrxml is maintained in SCM of course.
Java Developers build code that (look at the numbers in the diagram):
1. Decides which connection to supply to the main report. The data source comes as part of the report name. For the example it is “rt” which means realtime and so a local sqlite database will be populated using the same metadata iReport designers used for their tests (daily.sql). It can be a connection to a non realtime database for example a CRM database. Regardless of what connection we supply the “realTimeDbPath” will be passed to Jasper in the case any subreport needs a realtime connection.
2. Runs a Service#populateDb() method following a convention like for example “DailyReportService”. This service is in charge of preparing the local sqlite with all needed datasets for either the main report or subreports.
3. Resolves the name of the compiled report file to pull from the file system (rt_daily.jasper)
4. Supplies connection, parameters and jasper file to the Jasper Report Engine to get a pdf file with the results of the report. All parameters supplied as part of the form are supplied to the Jasper Engine BTW. This allows to minimize coding but also imposes a security concern. Be sure you do not rely just on parameters but on internal security at services layer. Spring Security with the help of AOP is ideal for this.
MVC pattern respected
View: (Report writting)Ideally someone that knows how the report should look will take care of this layer. This person just cares about the organization and layout of the data. One important assumption should be made at this point: The report writer shouldn’t necessarily be a DB developer, a High Level Language (HLL) developer (like Java, C# developers) or any other technical person. The report writter should have available certain datasources to visualize his report. The writer should be familiar with basic SQL concepts. The writter can be and should be IMO a Business Analyst (BA)
Model: Data
To get the report your company needs you will probably need to dig into Excel, SOAP services, text documents, databases, XML you name it. It makes sense though that this data gets translated to fixed tables from where the person in charge of the View could easily build the report. A DB developer is the best fit for this layer. A HLL programmer is a good fit as well. A BA can definitely build a reporting model as well. After all nobody better than him knows the dataset he will need per subreport.
Controller: Logic
An HLL developer will be needed for this layer. This is the layer in charge of all the plumbing between View and Model:
1. Uses a Services layer that in turn uses a DAO layer.
2. Implements security to determine which users have access to run which stored reports.
3. When the report is run it looks for the need of any real time data and if needed it populates it.
4. It invokes the Jasper Engine to run the particular report.
Local Environment
It is easier when everything is in a simple database and better when the data is de-normalized. For this document we are starting from three tables that will be de-normalized into just one. I am using MySQL here. Note that this is an example to illustrate complicated scenarios when you need to get data from different sources in just one data set. In reality if you have all you need in three different tables from the same database and you must provide real time reporting you are fine just pointing to the real database from ireport for developing the report and later naming the report with a proper datasource to be sure the needed connection is available at runtime.Let us say that our original DB has three tables
CREATE TABLE `office` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=latin1 CREATE TABLE `department` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=latin1 CREATE TABLE `employee` ( `id` int(11) NOT NULL AUTO_INCREMENT, `first_name` varchar(50) NOT NULL, `last_name` varchar(50) NOT NULL, `department_id` int(11) DEFAULT NULL, `office_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `FK_employee_office_id` (`office_id`), KEY `FK_employee_department_id` (`department_id`), CONSTRAINT `FK_employee_department_id` FOREIGN KEY (`department_id`) REFERENCES `department` (`id`), CONSTRAINT `FK_employee_office_id` FOREIGN KEY (`office_id`) REFERENCES `office` (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=latin1Let us say it has this data:
INSERT INTO `office` (name) VALUES ('Buenos Aires');
SET @office_id = last_insert_id();
INSERT INTO `department` (name) VALUES ('Engineering');
SET @department_id = last_insert_id();
INSERT INTO employee (first_name, last_name, office_id, department_id) values ('Pablo', "Cardone", @office_id, @department_id);
INSERT INTO `office` (name) VALUES ('Sao Paulo');
SET @office_id = last_insert_id();
INSERT INTO `department` (first_name) VALUES ('Marketing');
SET @department_id = last_insert_id();
INSERT INTO employee (first_name, last_name, office_id, department_id) values ('Ronaldo', "Gomes", @office_id, @department_id);
Our good practices sense tell us that for a report we are better providing just the whole dataset denormalized (just one table). We use sqlite3 for the local database. We must provide the necessary metadata and some initial data for iReport Designers. We will package metadata in a file called sampleEmployee.sql. This file will be accessible in the classpath of our application as we want to share it with report designers and ensure the same version is used from the application:CREATE TABLE `employee` ( `first_name` varchar(50) NOT NULL, `last_name` varchar(50) NOT NULL, `office_name` varchar(50) NOT NULL, `department_name` varchar(50) NOT NULL ) ;In a different file sampleEmployee_data.sql (not to be in the classpath of the application) we expose some sample data so the iReport users can start designing the layout:
INSERT INTO employee (first_name, last_name, office_name, department_name) VALUES ('Nestor','Urquiza','Buenos Aires','Engineering');
INSERT INTO employee (first_name, last_name, office_name, department_name) VALUES ('Pablo','Cardone','Sao Paulo','Sales');
INSERT INTO employee (first_name, last_name, office_name, department_name) VALUES ('Ronaldo','Gomes','Sao Paulo','Sales');
So locally our report writers can use the below command to get their testing data locally:cd ~/ sqlite3 sampleEmployee.db < sampleEmployee.db sqlite3 sampleEmployee.db < sampleEmployee_data.db
Designing the report with iReport
iDesiner is a visual tool that allows to design JasperReports. This is then your tool to create the Report Framework View side. I have tested this using iReport-3.7.4.1. Open iReport. Go to preferences and be sure in classpath and point to sqlite3 driver file: sqlitejdbc-v056.jar. Of course you need to download that file if you do not have it. Just Google it.
2. Select File | New | Report | Blank | Launch Report Wizard. In Connections pick a new Database JDBC connection and use the below settings (You can always reconfigure this from the Designer toolbar clicking on the Report Datasources icon):
name: sampleEmployee JDBC Driver: org.sqlite.JDBC JDBC URL: jdbc:sqlite:/Users/nestor/sampleEmployee.db
3. Hit Test button (username/password both are blank) and you should get a successful message. Hit Save. 4. In Query(SQL) paste this:
SELECT first_name, last_name, office_name, department_name FROM employee
4. Expand “Fields” on the left and drag each of them to the Detail portion of the report.
5. From the reports element palette on the right drag labels to the Column Header section.
6. Click on Preview and the report should show the two records we added initially to the employee table. When you hit “Preview” Jasper compiles the .jrxml file into a .jasper file. This .jasper file will be deployed in a reports folder that the application reads so the expensive report compilation will be done by the iReport user and we will reuse it later from our backend.
Implementation
We use Spring for dependency injection however for Jasper Reports we do not use Spring. Jasper provides enough simplicity, that is all. First you will need to include some dependencies. As I use Maven all I have to do is to include the below in my pom.xml:<dependency>
<groupId>net.sf.jasperreports</groupId>
<artifactId>jasperreports</artifactId>
<version>3.7.4</version>
</dependency>
Real time reporting needs a local database but concurrency makes it prohibited to have just one. Ideally User A running the sampleEmployee report should not be affected by User B who is running the same report. This is not a problem if the database is of a warehouse type meaning it gets populated asynchronously. In our case though we need to be sure we create tables per user for those cases where the data must be populated synchronously (real time) We address this using an individual database per user session. So we will have several local databases like: ...Each database will have a dataSource associated to it which is built on the fly by a DataSourceService. As you can see the local per user BD convention is “report_” + JSESSIONID + “.db” Given a URL like report/{dataSourceName}/{reportName} for example report/rt/sampleEmployee we can determine by conventions that the data source is to be built on the fly (instead of using one already injected by Spring). That is what real time (rt) stands for. The Controller can then instantiate a service following conventions (SampleEmployeeReportService) and then call a method populateDB() on it (of course it is implementing a custom ReportService interface) The service will use a DAO that will access the local sqlite DB, it will drop the table representing the report data set and it will populate with certain data that follows some business rules. The power here is unlimited as we play with a high level language like Java. Data can literally come from any place. The DAO uses Spring JDBC template to connect to the specific local user DB. It creates the metadata and fill out the table as well. Finally the Controller will invoke JasperReports to render the table content with the help of the sampleEmployee.jasper file created from iReport. Below is what I think is relevant from Java perspective. This is just a typical Controller class relying on injected Services.
report_17A1B49C645D39C2F2BE4CD12B54AF75.db report_E33819D245A598EDA01D1E3FC468EFE8.db
...
@Controller
public class ReportController extends RootController {
@Autowired
Properties applicationProperties;
@Autowired
ReportDataSourceService reportDataSourceService;
/**
* A filename for reports is composed of two tokens
* <datasource>_<description> If datasource == 'rt' the datasource will be a
* local to the server sqlite db built on the fly
*
*
* @param request
* @param response
* @param result
* @param reportType
* @param dataSourceName
* @param reportName
* @param model
* @return
* @throws IOException
*/
@RequestMapping("/report/{dataSourceName}/{name}")
public ModelAndView run(HttpServletRequest request,
HttpServletResponse response,
@ModelAttribute("report") Report report, BindingResult result,
@PathVariable("dataSourceName") String dataSourceName,
@PathVariable("name") String name, Model model) throws IOException {
// Initialize the context (mandatory)
ControllerContext ctx = new ControllerContext(request, response);
init(ctx);
String sessionId = request.getSession().getId();
// Bind to path variables
report.setDataSourceName(dataSourceName);
report.setName(name);
report.setSessionId(sessionId);
report.setParams(ctx.getParameterMapWithEncoding());
// If real time type get the Service bean and populate DB
try {
// name = name.substring(0, 1).toLowerCase()
// + name.substring(1);
ReportService reportService = (ReportService) applicationContext
.getBean(name + "ReportService");
reportService.populateData(report);
} catch (Throwable e) {
e.printStackTrace();
}
// Get proper parameters for jasper
Map<String, String> jasperParams = getParamsForJasper(report
.getParams());
// insert parameters commonly used by most reports
String realTimeDbPath = reportDataSourceService
.getRealTimeDatabasePath(report);
jasperParams.put("realTimeDbPath", realTimeDbPath);
// Get the master report datasource
DataSource dataSource = reportDataSourceService.getDataSource(report);
// Get reports path
String path = getReportsPath();
File reportFile = new File(path + "/" + dataSourceName + "_" + name
+ ".jasper");
byte[] bytes = null;
Connection connection = null;
try {
connection = dataSource.getConnection();
bytes = JasperRunManager.runReportToPdf(reportFile.getPath(),
jasperParams, connection);
response.setContentType("application/pdf");
response.setContentLength(bytes.length);
response.getOutputStream().write(bytes);
} catch (Exception e) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
e.printStackTrace(printWriter);
String stackTrace = stringWriter.toString();
result.addError(new ObjectError("report", stackTrace));
return getModelAndView(ctx, "report/error");
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return null;
}
@RequestMapping("/report/list")
public ModelAndView list(HttpServletRequest request,
HttpServletResponse response, Model model) throws IOException {
// Initialize the context (mandatory)
ControllerContext ctx = new ControllerContext(request, response);
init(ctx);
// Get report path
String path = getReportsPath();
// Get all available reports
File inFolder = new File(path);
FileFilter fileFilter = new FileFilter() {
public boolean accept(File file) {
return !file.isDirectory()
&& file.getName().endsWith(".jasper");
}
};
File[] files = inFolder.listFiles(fileFilter);
TreeMap<String, String> reports = new TreeMap<String, String>();
for (File file : files) {
String fullName = file.getName();
String baseName = fullName.substring(0, fullName.length() - 7);
String[] tokens = baseName.split("_");
if (tokens.length == 2) {
String url = tokens[0] + "/" + tokens[1];
String name = tokens[1];
reports.put(name, url);
}
}
model.addAttribute("reports", reports);
return getModelAndView(ctx, "report/list");
}
private String getReportsPath() throws IOException {
return (String) applicationProperties.get("jasper.reports.path");
}
/**
* Jasper will not accept more than one parameter named with the same name.
* We most likely will not need to pass complex objects to Jasper so we
* should be OK
*
* @param requestParams
* @return
*/
private Map<String, String> getParamsForJasper(
Map<String, List<String>> requestParams) {
Map<String, String> jasperParams = new HashMap<String, String>();
for (String key : requestParams.keySet()) {
jasperParams.put(key, requestParams.get(key).get(0));
}
return jasperParams;
}
Sub Reports
To illustrate subreports let us create a report that accepts a parameter, the full name of an employee. We can run for example a Bing query to see some public pages that might be related to each employee (like a custom and proprietary background check ;-).1. Establish the dataset you will need and script metadata. and data. Here is sampleCheck.sql:
CREATE TABLE `links` ( `full_name` varchar(50) NOT NULL, `title` varchar(250) NOT NULL, `url` varchar(50) NOT NULL ) ;
2. Script the data. Here is sampleCheck_data.sql
INSERT INTO links (full_name, title, url) VALUES ("Nestor Urquiza", "Nestor Urquiza", "http://www.bing.com:80/search?q=nestor+urquiza");
INSERT INTO links (full_name, title, url) VALUES ("Nestor Urquiza", "Nestor Urquiza Resume", "http://www.nestorurquiza.com/resume");
INSERT INTO links (full_name, title, url) VALUES ("Pablo Cardone", "Pablo Cardone", "http://www.bing.com:80/search?q=pablo+cardone");
INSERT INTO links (full_name, title, url) VALUES ("Pablo Cardone", "Pablo Cardone Resume", "http://www.pablocardone.com/resume");
3. Build a local DB to be used to design the report:
sqlite3 sampleCheck.db < sampleCheck.sql sqlite3 sampleCheck.db < sampleCheck_data.sql
4. At this point table “links” exists inside sampleCheck.db so open iReport and create a report called rt_sampleCheck. A file with extension .jrxml will be created. Use as data source the newly created DB. So:
name: sampleCheck
JDBC Driver: org.sqlite.JDBC
JDBC URL: jdbc:sqlite:/Users/nestor/sampleCheck.db
Query: select title, url from links where full_name = '$P!{fullName}';
5. Open iReport and include title and URL. Create a parameter called “fullName” Run the report. When asked for the parameter value use “Nestor Urquiza” The jasper file (rt_sampleCheck.jasper) is generated in the same directory where the jrxml file is.
6. Create the DAO implementation (SampleCheckReportDAO) that accepts a List of Objects to persist
7. Create a Service (SampleCheckReportService) that queries Bing for all users full names.
8. As we have follow conventions now it will be enough to call /report/rt/sampleCheck?fullName=Nestor+Urquiza from the browser. Note that we need a parameter this time. The parameter is inserted as is as a report param that is why the convention is so important here. Note also we could have live without a parameter in this simple example as we can restrict from java what we populate in the table however this is needed for our next final example. We will use sampleCheck as a subreport of sampleEmployee
Report plus Subreport
In reality both of the reports we have built so far use the same connection as they are both real time reports and so they use the same local sqlite database. However there are more complicated cases and I want to be sure I show here how a subreport can use a different connection than the main report. Once you add the subreport using the ireport GUI you will need to edit the XML as shown below. Note how I use a parameter to provide the location of the real time database. Of course I do so because I want to be sure I can inject that value later from Java.<subreport>
<reportElement x="14" y="35" width="200" height="100"/>
<subreportParameter name="fullName">
<subreportParameterExpression><![CDATA[$F{first_name} + " " + $F{last_name}]]></subreportParameterExpression>
</subreportParameter>
<connectionExpression><![CDATA[java.sql.DriverManager.getConnection("jdbc:sqlite:" + $P{realTimeDbPath}, "", "")]]></connectionExpression>
<subreportExpression class="java.lang.String"><![CDATA[$P{SUBREPORT_DIR} + "rt_sampleCheck.jasper"]]></subreportExpression>
</subreport>
The report when run will ask for the parameter and we will provide locally a value like below: /Users/nestor/sampleCheck.db
The parent report must have empty path to look for the subreport. The reason is that we will drop all reports in the same folder to avoid another parameter to be passed (the subreport path)
All that is left now is to run our main report from Java. So we need to provide as a parameter “realTimeDbPath”. From Java we will need to populate of course the subreport data source and that is why we call the sampleCheckService from sampleEmployeeService. We can check still /report/rt/sampleCheck?fullName=Nestor+Urquiza works. Now /report/rt/sampleEmployee is rendering the sampleCheck subreport as well. Note that sampleCheck subreport gets the fullName from the master report so there is no need to insert it from Java. Still this works both from iDesigner with no Server in the middle (ideal for report writters) and from Java which is of course needed to present dynamic real time data. It would be great if JasperReports would allow for the use of connectionExpression element at the main report level and not only at subreport level. Unfortunately that is not the case and that is why we need to supply the main report connection as part of the URL (Jasper Report engine will not be able to dynamically based on a parameter discover which connection to use for the master report) So for the master report we provide a connection object whereas for the subreports we use “connectionExpression”. The expression will need to use the “realTimeDbPath” parameter when the report uses a real time data set or will be completely hardcoded when using any SQL database. Note that datasources do not necessarily have to be hardcoded in subreports “connectionExpression” elements. We can always use parameters to build them on the fly as already explained.
Below is a snapshot of the pdf report obtained from the web request:
And here a snapshot of the same from iReport. Same JRXML used from the backend and the frontend without stepping on each other toes.
A Check List
Below is a check list for both iReport Designers (BA) and Java Developers. As a developer you will need to provide the iReport Designer with:1. script.sql
2. script_data.sql
3. Path where to put the compiled report file so it shows up from the web interface
4. Agree on a name for the report. Use Spring Resources for internationalization so when showing the name of the report it can show a more descriptive name but try hard to get a name that makes sense to everybody.
As a report Designer you will need to:
1. run the below commands:
sqlite3 local.db < script.sql sqlite3 local.db < script_data.sql
2. Create a report and use local.db from above as data source. Naming conventions is important. The report must be named using the data source name followed by underscore and then the name of the service agreed with the developer. Case matters, so be aware.
3. Configure subreport "subreportParameter" and "connectionExpression" nodes. Here is a list of useful connection expressions: realtime sqlite:
<connectionExpression><![CDATA[java.sql.DriverManager.getConnection("jdbc:sqlite:" + $P{realTimeDbPath}, "", "")]]></connectionExpression>
mysql: <connectionExpression><![CDATA[java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/mySQLDatabaseName", "myUserName", "myPassword")]]></connectionExpression>
sqlserver: <connectionExpression><![CDATA[java.sql.DriverManager.getConnection("jdbc:jtds:sqlserver://localhost:1433/sqlServerDatabaseName;prepareSQL=3", "myUserName", "myPassword")]]></connectionExpression>4. Put the results in the server reports directory.
You can download the Jasper sources and sqlite databases from here.
Saturday, September 18, 2010
Command Line Interface CLI from Spring
Command Line Interface (CLI) is useful when you want to script certain actions. While I prefer Perl and Python for this kind of tasks sometimes you have reduced team of developers that are just comfortable with one language. In addition what happens when you have a lot of libraries already built in that language? You might be able to interact with those still but let us face it native is faster and so better.
Let us say you are using Spring and have a lot of investment in Java. You can really easy integrate with your existing libraries and provide complex command line driven actions built completely in Java. You can later then run those commands still from Python, Ruby, Perl, AWK, bash etc.
The only thing you need to do from your main() method is to get access to the spring application context and get a service bean. The rest of annotations and definitions will be wired provided the spring context file allows for correct dependency injection. This is nothing different from what you would do in regular spring programming.
Below is a sample java class intended to rename files from an input folder to an output folder. The actual implementation is not provided as the only purpose of this post is clarify how to interconnect a main() java class with existing autowired (let's say from an included jar file) spring resources.
Here is the command line parser
Let us say you are using Spring and have a lot of investment in Java. You can really easy integrate with your existing libraries and provide complex command line driven actions built completely in Java. You can later then run those commands still from Python, Ruby, Perl, AWK, bash etc.
The only thing you need to do from your main() method is to get access to the spring application context and get a service bean. The rest of annotations and definitions will be wired provided the spring context file allows for correct dependency injection. This is nothing different from what you would do in regular spring programming.
Below is a sample java class intended to rename files from an input folder to an output folder. The actual implementation is not provided as the only purpose of this post is clarify how to interconnect a main() java class with existing autowired (let's say from an included jar file) spring resources.
package com.nestorurquiza.cli;
import java.text.ParseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import com.nestorurquiza.service.FilesService;
public class RenameFiles {
private final static Logger log = LoggerFactory.getLogger(RenameFiles.class);
/**
* @param args
* @throws ParseException
*/
public static void main(String[] args) {
log.info("Starting ...");
//get spring context first
ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("cli-context.xml");
//Get then a reference to a service bean
FilesService filesService = (FilesService) applicationContext.getBean("filesService");
// parse command line parameters to CmdParser
FilesCommandParser commandParser = new FilesCommandParser(args);
String inFolderPath = commandParser.getInFolderPath();
String outFolderPath = commandParser.getOutFolderPath();
//Invoke the service method that executes the task
fileService.renameFiles(inFolderPath, outFolderPath);
//Exit the shell process
log.info("... Done");
System.exit(0);
}
}
Here is the command line parser
package com.nestorurquiza.cli;
import org.apache.commons.cli.BasicParser;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
public class FilesCommandParser {
private Options options = new Options();
private CommandLine cl;
private String inFolderPath;
private String outFolderPath;
public FilesCommandParser(String[] args) {
options.addOption(new Option("inFolderPath", true, "input folder path"));
options.addOption(new Option("outFolderPath", true, "output folder path"));
parseCommandline(args);
help();
mandatory();
}
public String getInFolderPath() {
return inFolderPath;
}
public void setInFolderPath(String inFolderPath) {
this.inFolderPath = inFolderPath;
}
public String getOutFolderPath() {
return outFolderPath;
}
public void setOutFolderPath(String outFolderPath) {
this.outFolderPath = outFolderPath;
}
private void parseCommandline(String[] args) {
CommandLineParser parser = new BasicParser();
try {
cl = parser.parse(options, args);
} catch (ParseException e) {
System.out.println("Parsing failed. Reason: " + e.getMessage());
this.generateHelp();
System.exit(1);
}
}
private void generateHelp() {
HelpFormatter formatter = new HelpFormatter();
formatter.printHelp("RenameFiles", options);
}
private void help() {
if (cl.hasOption("Help")) {
this.generateHelp();
System.exit(0);
}
}
private void mandatory() {
if (!cl.hasOption("inFolderPath") || !cl.hasOption("outFolderPath")) {
this.generateHelp();
System.exit(0);
} else{
setInFolderPath(cl.getOptionValue("inFolderPath"));
setOutFolderPath(cl.getOptionValue("outFolderPath"));
}
}
}
Subscribe to:
Posts (Atom)





