Preventing Crystal Report's 'Report document has already been closed'
When you have a Java application which generates PDF documents based on a CR template you will notice that loading the template the first time will take quite some time. One thing you could do is loading the template as a (Spring) @Bean. Pretty soon you will notice that after about 15 minutes the report document will time-out and cannot be used to generate PDFs anymore.
The error which will be displayed is: com.crystaldecisions.sdk.occa.report.lib.ReportSDKException: The report document has already been closed.---- Error code:-2147467259 Error code name:failed
The only solution then is to stop your application and restart it again, which is unacceptable of course. Another solution is to put the bean into a certain scope. Spring however does not have a scope defined that suits this situation. But luckily, they can be implemented fairly easy.
Here is an example of a custom scope, which re-initialises beans after 10 minutes of inactivity:
package org.springframework.beans.scope;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class TimeoutScope implements Scope {
private static final long TIMEOUT_IN_MINUTES = 10 * 60 * 1000;
private Map<String, Object> objectMap = Collections.synchronizedMap(new HashMap<String, Object>());
private Map<Object, Long> timeoutMap = Collections.synchronizedMap(new HashMap<Object, Long>());
public Object get(String name, ObjectFactory<?> objectFactory) {
if (objectMap.containsKey(name)) {
Object object = objectMap.get(name);
long timeoutValue = timeoutMap.get(object);
if (System.currentTimeMillis() >= timeoutValue) {
objectMap.remove(name);
timeoutMap.remove(object);
}
}
if (!objectMap.containsKey(name)) {
Object object = objectFactory.getObject();
objectMap.put(name, object);
}
Object object = objectMap.get(name);
timeoutMap.put(object, System.currentTimeMillis() + TIMEOUT_IN_MINUTES);
return object;
}
public Object remove(String name) {
if (objectMap.containsKey(name)) {
Object object = objectMap.get(name);
timeoutMap.remove(object);
}
return objectMap.remove(name);
}
public void registerDestructionCallback(String name, Runnable callback) {
}
public Object resolveContextualObject(String key) {
return null;
}
public String getConversationId() {
return null;
}
}
Now you can add the scope to your configuration by for instance do the following (or write a XML equivalent for it):
@Bean
public static CustomScopeConfigurer getCustomScopes() {
CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer();
Map<String, Object> scopes = new HashMap<>();
scopes.put("timeout", new TimeoutScope());
customScopeConfigurer.setScopes(scopes);
return customScopeConfigurer;
}
And from that time you can add the @Scope(value = "timeout")
to the @Bean which loads your template.