2
votes

In my Sling app I have data presenting documents, with pages, and content nodes. We mostly server those documents as HTML, but now I would like to have a servlet to serve these documents as PDF and PPT.

Basically, I thought about implementing the factory pattern : in my servlet, dependending on the extension of the request (pdf or ppt), I would get from a DocumentBuilderFactory, the proper DocumentBuilder implementation, either PdfDocumentBuilder or PptDocumentBuilder.

So first I had this:

public class PlanExportBuilderFactory {

  public PlanExportBuilder getBuilder(String type) {
    PlanExportBuilder builder = null;
    switch (type) {
      case "pdf":
        builder = new PdfPlanExportBuilder();
        break;
      default: 
        logger.error("Unsupported plan export builder, type: " + type);
    }
    return builder;
  }
}

In the servlet:

@Component(metatype = false)
@Service(Servlet.class)
@Properties({ 
  @Property(name = "sling.servlet.resourceTypes", value = "myApp/document"), 
  @Property(name = "sling.servlet.extensions", value = { "ppt", "pdf" }),
  @Property(name = "sling.servlet.methods", value = "GET") 
})
public class PlanExportServlet extends SlingSafeMethodsServlet {

  @Reference
  PlanExportBuilderFactory builderFactory;

  @Override
  protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {

    Resource resource = request.getResource();

    PlanExportBuilder builder = builderFactory.getBuilder(request.getRequestPathInfo().getExtension());
  }
}    

But the problem is that in the builder I would like to reference other services to access Sling resources, and with this solution, they're not bound.

I looked at Services Factory with OSGi but from what I've understood, you use them to configure differently the same implementation of a service.

Then I found that you can get a specific implementation by naming it, or use a property and a filter.

So I've ended up with this:

public class PlanExportBuilderFactory {

  @Reference(target = "(builderType=pdf)")
  PlanExportBuilder pdfPlanExportBuilder;

  public PlanExportBuilder getBuilder(String type) {
    PlanExportBuilder builder = null;
    switch (type) {
      case "pdf":
        return pdfPlanExportBuilder;
      default: 
        logger.error("Unsupported plan export builder, type: " + type);
    }
    return builder;
  }

}

The builder defining a "builderType" property :

// AbstractPlanExportBuilder implements PlanExportBuilder interface
@Component
@Service(value=PlanExportBuilder.class)
public class PdfPlanExportBuilder extends AbstractPlanExportBuilder {

  @Property(name="builderType", value="pdf")

  public PdfPlanExportBuilder() {
    planDocument = new PdfPlanDocument();
  }
}

I would like to know if it's a good way to retrieve my PDF builder implementation regarding OSGi good practices.

EDIT 1

From Peter's answer I've tried to add multiple references but with Felix it doesn't seem to work:

 @Reference(name = "planExportBuilder", cardinality = ReferenceCardinality.MANDATORY_MULTIPLE, policy = ReferencePolicy.DYNAMIC)
private Map<String, PlanExportBuilder> builders = new ConcurrentHashMap<String, PlanExportBuilder>();

protected final void bindPlanExportBuilder(PlanExportBuilder b, Map<String, Object> props) {
  final String type = PropertiesUtil.toString(props.get("type"), null);
  if (type != null) {
    this.builders.put((String) props.get("type"), b);
  }
}

protected final void unbindPlanExportBuilder(final PlanExportBuilder b, Map<String, Object> props) {
  final String type = PropertiesUtil.toString(props.get("type"), null);
  if (type != null) {
    this.builders.remove(type);
  }
}

I get these errors :

@Reference(builders) : Missing method bind for reference planExportBuilder
@Reference(builders) : Something went wrong: false - true - MANDATORY_MULTIPLE
@Reference(builders) : Missing method unbind for reference planExportBuilder

The Felix documentation here http://felix.apache.org/documentation/subprojects/apache-felix-maven-scr-plugin/scr-annotations.html#reference says for the bind method:

The default value is the name created by appending the reference name to the string bind. The method must be declared public or protected and take single argument which is declared with the service interface type

So according to this, I understand it cannot work with Felix, as I'm trying to pass two arguments. However, I found an example here that seems to match what you've suggested but I cannot make it work: https://github.com/Adobe-Consulting-Services/acs-aem-samples/blob/master/bundle/src/main/java/com/adobe/acs/samples/services/impl/SampleMultiReferenceServiceImpl.java

EDIT 2 Just had to move the reference above the class to make it work:

@References({
  @Reference(
    name = "planExportBuilder",
    referenceInterface = PlanExportBuilder.class,
    policy = ReferencePolicy.DYNAMIC,
    cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE)
})
public class PlanExportServlet extends SlingSafeMethodsServlet {
1

1 Answers

2
votes

Factories are evil :-) Main reason is of course the yucky class loading hacks that are usually used but also because they tend to have global knowledge. In general, you want to be able to add a bundle with a new DocumentBuilder and then that type should become available.

A more OSGi oriented solution is therefore to use service properties. This could look like:

@Component( property=HTTP_WHITEBOARD_FILTER_REGEX+"=/as")
public class DocumentServlet {

  final Map<String,DocBuilder>        builders = new ConcurrentHashMap<>();

  public void doGet( HttpServletRequest rq, HttpServletResponse rsp ) 
                           throws IOException, ServletException {

    InputStream in = getInputStream( rq.getPathInfo() );
    if ( in == null ) 
      ....

    String type = toType( rq.getPathInfo(), rq.getParameter("type") );

    DocBuilder docbuilder = builders.get( type );
    if ( docbuilder == null)
       ....

    docbuilder.convert( type, in, rsp.getOutputStream() );
 }

 @Reference( cardinality=MULTIPLE, policy=DYNAMIC )
 void addDocBuilder( DocBuilder db, Map<String,Object> props ) {
    docbuilders.put(props.get("type"), db );
 }

 void removeDocBuilder(Map<String,Object> props ) {
    docbuilders.remove(props.get("type"));
 }

}

A DocBuilder could look like:

@Component( property = "type=ppt-pdf" )
public class PowerPointToPdf implements DocBuilder {
    ...
}