Proposed changes to Fop configuration and deployment (the o.a.f.apps API)
The following is a proposal to modify some parts of the FOP API that are used for configuring and executing the FOP process (e.g fo -> pdf). The change is designed to make the separation of configuration and deployment both explicit and enforceable.
Current API
The FOP process is executed using an instance of o.a.f.apps.Fop, and typically triggered from an XSLT transform:
transformer.transform(new StreamSource(inputFoFile), fop.getDefaultHandler())
Many configurable options are available to customize the process. e.g. enabling accessibility in PDF output. Configuration can be shared amongst different instances of FOP, and this is coordinated with a FopFactory and a FOUserAgent. To describe how these classes collaborate, lets consider a fairly general use case
FopFactory fopFactory = FopFactory.newInstance();
fopFactory.setX;
fopFactory.setUserConfig(fopConfFile);
//Triggers a parse of the fop conf file and the setting of properties on the fopFactory
FOUserAgent foUserAgent = fopFactory.newFOUserAgent();
foUserAgent.set...
fopFactory.set... //!!!
Fop fop = fopFactory.newFop(outputFormat, foUserAgent, mimeType);
// trigger FOP process
The Fop object has access to the FOUserAgent and the FopFactory (exposed by FOUserAgent)
From line 4 we see that the is a one-to-many relationship between FopFactory and FOUserAgent; The intended use of this API is to create one FOUserAgent for each Fop instance, however there is no restriction in place to ensure this, neither intent to change this behaviour in the near future.
The code demonstrates that the configuration of the FopFactory can be interleaved with the construction of the Fop instance, and furthermore, the internal FOP process can change properties of the FopFactory (and FOUserAgent) and making assertions about the state of Fop is made difficult.
Proposed changes
We propose a change to the API to impose that the configuration and creation of Fop is done in a strict sequential order. To orchestrate this we have introduced a few new classes, notably FopConfParser and FopFactoryBuilder (plus others explained below). An example best demonstrates this:
// Parse the fopConf, setting properties on the FopFactoryBuilder
FopFactoryBuilder fopFactoryBuilder = new FopConfParser(userConfigFile).getFopFactoryBuilder()
fopFactoryBuilder.setX;
FopFactory fopFactory = fopFactoryBuilder.build();
fopFactoryBuilder.setX // ! throws an IllegalStateException !
Note that now, once built, the FopFactory properties cannot be reassigned (although they may be references to mutable data, of course).
The FOUserAgent no longer provides access to the FopFactory, but instead provides the read-only properties directly ( FOUserAgent.getFopFactory().getX() becomes FOUserAgent.getX(). Further more, for backwards compatibility the static members newFop(String) and newFop(String, OutputStream) have been added (these are required by o.a.f.cli.InputHandler). Other than than the removal of getFopFactory(), changes to FOUserAgent will have no impact upon Fop client code.
At this stage, other than the FopFactory (and possible URI resolution related ones)), access to properties that are exclusive to the FOUserAgent will not change.
The FopConfParser replaces FopFactoryConfigurator: it delegates to the Avalon framework to parse the XML configuration and sets properties on it's FopFactoryBuilder.
FopFactory still exposes static factory methods to create instances when no programmatic configuration of a FopFactoryBuilder is required. These are:
public static FopFactory newInstance(String fopConfFile);
public static FopFactory newInstance(URI baseURI);
The last method is introduced as part of Unifying URI Resolution
Single configuration, single run
Currently FOP trunk reads the "generic" configuration (i.e. non-renderer specific config) when the FopFactory is instantiated, but postpones the reading of renderer specific config until it is necessary (i.e. when a FOP run is invoked). The configuration information isn't cached, so the renderer-specific config info is read on every FOP run. This isn't a major problem on the command-line however, in an embedded environment this can be costly. Presumably, some of this cost was mitigated by creating a font-caching system. However, in a highly restricted environment, FOP may not be allowed to create serialized caches in a temporary place for a variety of reasons.
The solution to this is fairly simple, to redesign the configuration to favour the embedded use-case and allow for the CLI font-caching as well. We have done this by caching all the parsed FOP conf information into an object that is controlled by the FOUserAgent. This is a lazy loaded cache so that the renderer specific config is only parsed when that renderer is invoked, however, once invoked, that specific config will not be parsed again. This has an additional benefit of making the font cache redundant in the embedded use case, since costs of parsing config is only done once in either case.
However, the font-cache isn't made redundant in the CLI use-case since that config doesn't persist. As such we kept the font-cache mechanism albeit with changes to how the font-caching set-up works which will be discussed further.
The FOP Environment
FOP is dependent on the environment it is invoked within, the requirement of system fonts, image libraries and such affect FOP output. The image libraries can't easily be controlled at present, however, system fonts and font detection are controlled by the o.a.f.fonts.FontManager. The font auto-detection mechanism - searching and/or recursing through specifield directories and system directories - use the font-cache to mitigate the performance cost and make the data persist between JVM invocations. These work perfectly fine (not including Batik and SVG fonts) from the CLI, however in more restrictive environments these can be an issue. In a multi-tenant environment, font availability has to be tightly controlled due to licensing restrictions; also users can't be allowed to access non-authorized directories.
As such we have created an EnvironmentProfile that is designed to represent the services and limitations of the environment in which FOP is invoked. This may be given to either the FopConfParser or the FopFactoryBuilder (either of the FopFactory builder classes) which wraps the ResourceResolver, FontManager and base-uri. The ResourceResolver and base-uri are intrinsically linked to the runtime environment and bu allowing clients to control I/O and resource acquisition. The FontManager controls the font caching, auto-detection and system font detection and as such its services are bound to the environment.
The FontManager works in the same way as it did previously, except the FontDetector and FontCacheManager can be injected into its constructor (I'm not going to preach the virtues of dependency injection). This allows us to create services that restrict these behaviours by implementing their respective interfaces with whatever restrictions a particular environment needs.
Questions
Is there a use case for setting properties on the FopFactoryBuilder before parsing the fopConf? - this is a trivial change left out to simplify the API: we would just need to implement new FopConfParser(args..., FopFactoryBuilder)