2
\$\begingroup\$

I am refactoring a legacy codebase to use spring. I am newish to spring and was wondering if you have any suggestions for the following code I refactored. Specifically about providing the ExpeditionEntitiesService.

Here is a REST service which uploads a file:

public class myRestService {
     @POST
     public Response upload(//params) {
         // do work
         ExpeditionEntities expeditionEntities = expeditionService.getExpeditionEntities(processController.getExpeditionId());
        ExpeditionEntitiesService expeditionEntitiesService = new ExpeditionEntitiesService(expeditionEntities, processController, bcidService, expeditionService);
        // run the triplifier
        Triplifier triplifier = new Triplifier(outputPrefix, uploadPath(), processController, expeditionEntitiesService);

        boolean runDeepRoots = Boolean.valueOf(settingsManager.retrieveValue("deepRoots"));

        triplifier.run(processController.getValidation().getSqliteFile());
         // more work
     }
}

The next class is used to convert the file to triples to be stored in Apache fuseki TBD:

public class Triplifier {

    public Connection connection;

    private String outputFolder;
    private Model model;
    private String tripleOutputFile;
    private String filenamePrefix;
    private ProcessController processController;
    private final ExpeditionEntitiesService expeditionEntitiesService;

    private static Logger logger = LoggerFactory.getLogger(Triplifier.class);

    /**
     * triplify dataset on the tabularDataReader, writing output to the specified outputFolder and filenamePrefix
     *
     * @param filenamePrefix
     * @param outputFolder
     */
    public Triplifier(String filenamePrefix, String outputFolder,
                      ProcessController processController, ExpeditionEntitiesService expeditionEntitiesService) {
        this.outputFolder = outputFolder;
        this.filenamePrefix = filenamePrefix;
        this.processController = processController;
        this.expeditionEntitiesService = expeditionEntitiesService;
    }

    public String getOutputFolder() {return outputFolder;}

    public String getFilenamePrefix() {return filenamePrefix;}

    public Model getModel() {return model;}

    public String getTripleOutputFile() {return tripleOutputFile;}

    /**
     * Return triples
     *
     * @return
     */
    private void getTriples() {
        System.gc();
        String status = "\tWriting Temporary Output ...";
        processController.appendStatus(status + "<br>");

        // Write the model
        model = new ModelD2RQ(FileUtils.toURL(getMapping()),
                FileUtils.langN3, "urn:x-biscicol:");
        model.setNsPrefix("ark", "http://ezid.cdlib.org/id/ark");
        // Write the model as simply a Turtle file
        File tripleFile = PathManager.createUniqueFile(filenamePrefix + ".n3", outputFolder);
        try {
            FileOutputStream fos = new FileOutputStream(tripleFile);
            model.write(fos, FileUtils.langNTriple, null);
            fos.close();
        } catch (FileNotFoundException e) {
            throw new FimsRuntimeException(500, e);
        } catch (IOException e) {
            logger.warn("IOException thrown trying to close FileOutputStream object.", e);
        }
        tripleOutputFile = outputFolder + File.separator + tripleFile.getName();

        if (tripleFile.length() < 1)
            throw new FimsRuntimeException("No triples to write!", 500);
    }

    /**
     * Construct the mapping file for D2RQ to read
     *
     * @return
     */
    private String getMapping() {
        connection.verifyFile();

        File mapFile = PathManager.createUniqueFile(filenamePrefix + ".mapping.n3", outputFolder);
        try {
            PrintWriter pw = new PrintWriter(mapFile);
            TabularDataReader tdr = processController.getValidation().getTabularDataReader();
            Mapping mapping = processController.getMapping();
            new D2RQPrinter(pw, connection, expeditionEntitiesService).printD2RQ(tdr.getColNames(), mapping);
            pw.close();
        } catch (FileNotFoundException e) {
            throw new FimsRuntimeException(500, e);
        }
        return outputFolder + File.separator + mapFile.getName();
    }

    /**
     * Run the triplifier using this class
     */
    public boolean run(File sqlLiteFile) {
        String status = "Converting Data Format ...";
        processController.appendStatus(status + "<br>");

        this.connection = new Connection(sqlLiteFile);
        getTriples();
        return true;
    }
}

The next class is used by the Triplifier to generate the mapping file used to create the triples:

public class D2RQPrinter {
    private PrintWriter pw;
    private Connection connection;
    private final ExpeditionEntitiesService expeditionEntitiesService;

    public D2RQPrinter(PrintWriter pw, Connection connection, ExpeditionEntitiesService expeditionEntitiesService) {
        this.pw = pw;
        this.connection = connection;
        this.expeditionEntitiesService = expeditionEntitiesService;
    }

    /**
     * Generate D2RQ Mapping Language representation of this Mapping's connection, entities and relations.
     */
    public void printD2RQ(List<String> colNames, Mapping mapping) {
        printPrefixes();
        printConnectionD2RQ();
        for (Entity entity : mapping.getEntities())
            printEntityD2RQ(entity, colNames);
        for (Relation relation : mapping.getRelations()) {
            printRelationD2RQ(relation, mapping);
        }
    }

    /**
     * Generate D2RQ Mapping Language representation of this Relation.
     *
     */
    public void printRelationD2RQ(Relation relation, Mapping mapping) {

        Entity subjEntity = mapping.findEntity(relation.getSubject());
        Entity objEntity = mapping.findEntity(relation.getObject());

        if (subjEntity == null || objEntity == null)
            return;

        String subjClassMap = getClassMap(subjEntity);
        String objClassMap = getClassMap(objEntity);

        pw.println("map:" + subjClassMap + "_" + objClassMap + "_rel" + " a d2rq:PropertyBridge;");
        pw.println("\td2rq:belongsToClassMap " + "map:" + subjClassMap + ";");
        pw.println("\td2rq:property <" + relation.getPredicate() + ">;");
        pw.println(getPersistentIdentifierMapping(objEntity));
        pw.println("\td2rq:condition \"" + objEntity.getWorksheetUniqueKey() + " <> ''\";");
        pw.println("\t.");
    }

    /**
     * Generate D2RQ Mapping Language ClassMap name for a given Entity.
     *
     * @return D2RQ Mapping ClassMap name.
     */
    private String getClassMap(Entity entity) {
        return entity.getWorksheet() + "_" + entity.getWorksheetUniqueKey() + "_" + entity.getConceptAlias();
    }

    /**
     * Generate D2RQ Mapping Language representation of this Entity with Attributes.
     */
    private void printEntityD2RQ(Entity entity, List<String> colNames) {
        pw.println("map:" + getClassMap(entity) + " a d2rq:ClassMap;");
        pw.println("\td2rq:dataStorage " + "map:database;");
        pw.println(getPersistentIdentifierMapping(entity));
        pw.println("\td2rq:class <" + entity.getConceptURI() + ">;");
        // ensures non-null values ... don't apply if this is a hash
        if (!entity.getColumn().contains("hash"))
            pw.println("\td2rq:condition \"" + entity.getColumn() + " <> ''\";");

        // TODO: add in extra conditions (May not be necessary)
        //pw.println(getExtraConditions());
        pw.println("\t.");

        // Get a list of colNames that we know are good from the spreadsheet
        // Normalize the column names so they can be mapped according to how they appear in SQLite
        ArrayList<String> normalizedColNames = new ArrayList<String>();
        Iterator it = colNames.iterator();
        while (it.hasNext()) {
            String colName = (String) it.next();
            normalizedColNames.add(colName.replace(" ", "_").replace("/", ""));
        }

        // Loop through attributes associated with this Entity
        if (entity.getAttributes().size() > 0) {
            for (Attribute attribute : entity.getAttributes())
                printAttributeD2RQ(attribute, entity, normalizedColNames);
        }
    }

    /**
     * * Generate D2RQ Mapping Language representation of this Attribute.
     *
     * @param parent
     * @param colNames
     */
    private void printAttributeD2RQ(Attribute attribute, Entity parent, List<String> colNames) {

        String classMap = getClassMap(parent);
        String table = parent.getWorksheet();
        String classMapStringEquivalence = "";

        Boolean runColumn = false;

        if (colNames.contains(attribute.getColumn())) {
            runColumn = true;
        }

        // Only print this column if it is in a list of colNames
        if (runColumn) {
            String classMapString = "map:" + classMap + "_" + attribute.getColumn();
            pw.println(classMapString + " a d2rq:PropertyBridge;");
            pw.println("\td2rq:belongsToClassMap " + "map:" + classMap + ";");
            pw.println("\td2rq:property <" + attribute.getUri() + ">;");
            pw.println("\td2rq:column \"" + table + "." + attribute.getColumn() + "\";");
            pw.println("\td2rq:condition \"" + table + "." + attribute.getColumn() + " <> ''\";");
            // Specify an equivalence, which is isDefinedBy
            classMapStringEquivalence = classMapString + "_Equivalence";
            pw.println("\td2rq:additionalPropertyDefinitionProperty " + classMapStringEquivalence + ";");
            pw.println("\t.");

            // Always use isDefinedBy, even if the user has not expressed it explicitly.  We do this by
            // using the uri value if NO isDefinedBy is expressed.
            pw.println(classMapStringEquivalence + " a d2rq:AdditionalProperty;");
            pw.println("\td2rq:propertyName <" + attribute.getIsDefinedByURIString() + ">;");
            if (attribute.getDefined_by()!= null) {
                pw.println("\td2rq:propertyValue <" + attribute.getDefined_by() + ">;");
            } else {
                pw.println("\td2rq:propertyValue <" + attribute.getUri() + ">;");
            }
            pw.println("\t.");
            /*
           Loop multi-value columns
           This is used when the Configuration file indicates an attribute that should be composed of more than one column
            */
        } else if (attribute.getColumn().contains(",")) {

            // TODO: clean this up and integrate with above code.
            String tempColumnName = attribute.getColumn().replace(",", "");

            String[] columns = attribute.getColumn().split(",");

            // Check if we should run this -- all columns need to be present in colNames list
            Boolean runMultiValueColumn = true;
            for (int i = 0; i < columns.length; i++) {
                if (!colNames.contains(columns[i])) {
                    runMultiValueColumn = false;
                }
            }

            // Only run this portion if the tempColumnName appears
            if (runMultiValueColumn) {

                String classMapString = "map:" + classMap + "_" + tempColumnName;
                pw.println(classMapString + " a d2rq:PropertyBridge;");
                pw.println("\td2rq:belongsToClassMap " + "map:" + classMap + ";");
                pw.println("\td2rq:property <" + attribute.getUri() + ">;");

                // Construct SQL Expression
                StringBuilder result = new StringBuilder();

                // Call this a sqlExpression
                result.append("\td2rq:sqlExpression \"");

                // Append ALL columns together using the delimiter... ALL are required
                if (attribute.getType().equals("all")) {
                    for (int i = 0; i < columns.length; i++) {
                        if (i != 0)
                            result.append(" || '" + attribute.getDelimited_by() + "' || ");
                        // Set required function parameters
                        if (attribute.getType().equals("all"))
                            pw.println("\td2rq:condition \"" + table + "." + columns[i] + " <> ''\";");
                        result.append(columns[i]);
                    }
                    result.append("\";");
                }

                // This is the YMD case using a very special SQLIte function to format data
                // Assume that columns are Year, Month, and Day EXACTLY
                else if (attribute.getType().equals("ymd")) {
                    // Require Year
                    pw.println("\td2rq:condition \"" + table + "." + columns[0] + " <> ''\";");

                    result.append("yearCollected ||  ifnull(nullif('-'||substr('0'||monthCollected,-2,2),'-0') || " +
                            "ifnull(nullif('-'||substr('0'||dayCollected,-2,2),'-0'),'')" +
                            ",'') ");
                    result.append("\";");

                }

                pw.println(result.toString());

                //pw.println("\td2rq:column \"" + table + "." + column + "\";");
                //pw.println("\td2rq:condition \"" + table + "." + column + " <> ''\";");

                // Specify an equivalence, which is isDefinedBy
                classMapStringEquivalence = classMapString + "_Equivalence";
                pw.println("\td2rq:additionalPropertyDefinitionProperty " + classMapStringEquivalence + ";");
                pw.println("\t.");

                // Always use isDefinedBy, even if the user has not expressed it explicitly.  We do this by
                // using the uri value if NO isDefinedBy is expressed.
                pw.println(classMapStringEquivalence + " a d2rq:AdditionalProperty;");
                pw.println("\td2rq:propertyName <" + attribute.getIsDefinedByURIString() + ">;");
                if (attribute.getDefined_by() != null) {
                    pw.println("\td2rq:propertyValue <" + attribute.getDefined_by() + ">;");
                } else {
                    pw.println("\td2rq:propertyValue <" + attribute.getUri() + ">;");
                }
                pw.println("\t.");

            }
        }
    }


    /**
     * Generate D2RQ Mapping Language representation of this Connection.
     */
    private void printConnectionD2RQ() {
        pw.println("map:database a d2rq:Database;");
        pw.println("\td2rq:jdbcDriver \"" + connection.system.driver + "\";");
        pw.println("\td2rq:jdbcDSN \"" + connection.getJdbcUrl() + "\";");
        if (connection.username != null && !connection.username.isEmpty())
            pw.println("\td2rq:username \"" + connection.username + "\";");
        if (connection.password != null && !connection.password.isEmpty())
            pw.println("\td2rq:password \"" + connection.password + "\";");
        pw.println("\td2rq:fetchSize \"" + (connection.system == DBsystem.mysql ? Integer.MIN_VALUE : 500) + "\";");
        pw.println("\t.");
    }

    /**
     * Generate all possible RDF prefixes.
     */
    private void printPrefixes() {
        // TODO: Allow configuration files to specify namespace prefixes!
        pw.println("@prefix map: <" + "" + "> .");
        pw.println("@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .");
        pw.println("@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .");
        pw.println("@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .");
        pw.println("@prefix d2rq: <http://www.wiwiss.fu-berlin.de/suhl/bizer/D2RQ/0.1#> .");
        pw.println("@prefix jdbc: <http://d2rq.org/terms/jdbc/> .");
        pw.println("@prefix ro: <http://www.obofoundry.org/ro/ro.owl#> .");
        pw.println("@prefix bsc: <http://biscicol.org/terms/index.html#> .");
        pw.println("@prefix urn: <http://biscicol.org/terms/index.html#> .");
        // TODO: update this prefix to EZID location when suffixPassthrough is ready
        pw.println("@prefix ark: <http://biscicol.org/id/ark:> .");


        pw.println();
    }

    /**
     * Sets the URI as a identifier to a column, or not, according to D2RQ conventions
     *
     * @param entity
     * @return
     */
    private String getPersistentIdentifierMapping(Entity entity) {
        String identifier = expeditionEntitiesService.getEntityIdentifier(entity);

        return "\td2rq:uriPattern \"" + identifier + "@@" + entity.getColumn() + "@@\";";

    }
}

The next class is used to get the unique persistent identifier for the triples:

public class ExpeditionEntitiesService {
    private static String DEFAULT_IDENTIFIER_PREFIX = "urn:x-biscicol:";
    private final ExpeditionEntities expeditionEntities;
    private final ProcessController processController;
    private final BcidService bcidService;
    private final ExpeditionService expeditionService;

    public ExpeditionEntitiesService(ExpeditionEntities expeditionEntities, ProcessController processController,
                                     BcidService bcidService, ExpeditionService expeditionService) {
        this.expeditionEntities = expeditionEntities;
        this.processController = processController;
        this.bcidService = bcidService;
        this.expeditionService = expeditionService;
    }

    public String getEntityIdentifier(Entity entity) {
        // Use the DeepRoots System to lookup Key
        String identifier = null;
        if (expeditionEntities != null) {
            identifier = lookupPrefix(entity);
        }

        // Use the default namespace value if dRoots is unsuccessful...
        if (identifier == null) {
            identifier = DEFAULT_IDENTIFIER_PREFIX + entity.getConceptAlias() + ":";
        }
        return identifier;
    }
        /**
         * Find the appropriate identifier for a concept contained in this file
         *
         * @return returns the Bcid for Entity resourceType in this DeepRoots file
         */
    public String lookupPrefix(Entity entity) {
        HashMap<String, String> entities = expeditionEntities.getEntities();
        Iterator it = entities.entrySet().iterator();
        while (it.hasNext()) {
            Map.Entry pairs = (Map.Entry) it.next();
            if (pairs.getKey().toString().trim().equals(entity.getConceptAlias().trim())) {
                String postfix = (String) pairs.getValue();
                return postfix;
            }
        }
        FimsPrinter.out.println("\tWarning: " + entity.getConceptAlias() + " cannot be mapped in Deep Roots, attempting to create mapping");
        // Create a mapping in the deeproots system for this URI
        FimsPrinter.out.println("\tCreating bcid root for " + entity.getConceptAlias() + " with resource type = " + entity.getConceptURI());

        Bcid bcid = new Bcid.BcidBuilder(entity.getConceptAlias())
                .title(entity.getConceptAlias())
                .build();

        bcidService.create(bcid, processController.getUserId());

        Expedition expedition = expeditionService.getExpedition(processController.getExpeditionCode(), processController.getProjectId());
        bcidService.attachBcidToExpedition(bcid, expedition.getExpeditionId());

        // Add this element to the entities string so we don't keep trying to add it in the loop above
        entities.put(entity.getConceptAlias(), String.valueOf(bcid.getIdentifier()));
        System.out.println("\tNew identifier = " + bcid.getIdentifier());
        return String.valueOf(bcid.getIdentifier());
    }
}
\$\endgroup\$
7
  • \$\begingroup\$ What exactly are your goals restructuring this code into Spring? Inversion of control? \$\endgroup\$ May 21, 2016 at 15:19
  • \$\begingroup\$ I guess I don't need to restructure this into spring. We recently moved to spring + hibernate. This is older code and I am looking to a way to integrate it with our new code better. Previously the code in ExpeditionEntitiesService.lookupPrefix which creates a new Bcid, would instantiate a new class, and call the class methods to persist the Bcid. When we migrated to spring, we are phasing out this old BcidCreator class in favor of spring data repositories and services (separating out business logic from persistence logic). \$\endgroup\$
    – rodney757
    May 21, 2016 at 20:57
  • \$\begingroup\$ While I don't necessarily need any of the above classes to be managed by spring, I do need to use spring managed classes in them. My main concern is injecting the ExpeditionEntitiesService into the REST controller and then passing the ExpeditionEntitesService into methods that don't directly use it, just pass it along. This is because where D2RQPrinter needs the ExpeditionEntitesService, but not the Triplifier. \$\endgroup\$
    – rodney757
    May 21, 2016 at 21:00
  • \$\begingroup\$ Do you have plans on changing your Web framework to Spring MVC? \$\endgroup\$ May 22, 2016 at 13:28
  • \$\begingroup\$ maybe eventually, but currently we are using jersey \$\endgroup\$
    – rodney757
    May 23, 2016 at 13:16

1 Answer 1

1
\$\begingroup\$

Garbage collection

System.gc();

Why do you need this?

return type

public boolean run(File sqlLiteFile) {
    // ...
    return true;
}

Do you really need to return true here?

try-with-resources

It's recommended to wrap your PrintWriter in try-with-resources for safe and efficient handling of the underlying I/O resource.

Formatting consistency

It's also recommended to use braces { } consistently for all code blocks, including one-liners. This makes it easier to understand the scope and prevents any accidental bugs in the future due to the conversion of one-liners to multi-liners.

Interfaces over implementations for type declaration

HashMap<String, String> entities = expeditionEntities.getEntities();

A HashMap is just a straightforward implementation of the Map interface, so declaring the return type of ExpeditionEntities.getEntities() as a Map<String, String> will work equally well. In fact, it is better in the sense that callers do not need to know they are interacting with a specific implementation, and that gives you some leeway to change implementations in the future (not a strong point, but nevertheless a valid one).

Of course, by changing the method return type, you will likely encounter breaks around your codebase as a Map can't be casted into a HashMap reference, but I think this is a clean break to have.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Thanks for the tips above. I didn't notice the garbage collection or the return type. It probably isn't needed. For some reason I though try-with-resources was java 8, but it looks like it is in 7 as well. I'll start using that as well as implement the other suggestions you made. Thanks \$\endgroup\$
    – rodney757
    May 21, 2016 at 21:16

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.