Last Updated: February 25, 2016
·
1.89K
· leviter

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.