Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 390 external instances #394

Merged
merged 16 commits into from
Jan 8, 2019
Merged

Issue 390 external instances #394

merged 16 commits into from
Jan 8, 2019

Conversation

dcbriccetti
Copy link
Contributor

@dcbriccetti dcbriccetti commented Dec 28, 2018

Closes #390

What has been done to verify that this works as intended?

New tests using @cooperka’s forms are passing. Editing, saving, and re-editing Kevin’s forms in Collect, with this change, getodk/collect#2771, appears to be working.

Why is this the best possible solution? Were any other approaches considered?

I’ve filled in the missing pieces, now that I better understand how things are supposed to work.

Are there any risks to merging this code? If so, what are they?

This changes where JavaRosa looks for external secondary instance files, so any client relying on the previous behavior will need to be changed.

…atches that of an internal secondary instance.
…sponding to the test class’s package name (as is our convention).

No coding changes are needed because the method we use to locate the files, `r`, looks in the package name directory and then in `resources`.
…instance files.

Add overloaded `getFormFromInputStream(InputStream is, String externalInstancePathPrefix)` to XFormUtils.
Modify test code, FormParserHelper, to use XFormUtils.getFormFromInputStream so we are testing the same path that Collect uses.
Added overloaded `ResourcePathHelper.r(String filename, boolean fallBack)` to allow building names of files that don’t exist (such as those that are generated).
@codecov-io
Copy link

codecov-io commented Dec 28, 2018

Codecov Report

Merging #394 into master will increase coverage by 0.14%.
The diff coverage is 55.38%.

Impacted file tree graph

@@             Coverage Diff              @@
##             master     #394      +/-   ##
============================================
+ Coverage     48.31%   48.45%   +0.14%     
- Complexity     2871     2883      +12     
============================================
  Files           239      239              
  Lines         13516    13555      +39     
  Branches       2625     2615      -10     
============================================
+ Hits           6530     6568      +38     
- Misses         6148     6149       +1     
  Partials        838      838
Impacted Files Coverage Δ Complexity Δ
...rosa/core/reference/InvalidReferenceException.java 75% <ø> (+75%) 1 <0> (+1) ⬆️
...g/javarosa/core/reference/ReferenceDataSource.java 0% <0%> (ø) 0 <0> (ø) ⬇️
...org/javarosa/core/reference/ResourceReference.java 27.77% <0%> (+6.72%) 3 <0> (+1) ⬆️
src/org/javarosa/xform/util/XFormUtils.java 44.44% <100%> (+1.79%) 14 <0> (ø) ⬇️
...arosa/core/reference/ResourceReferenceFactory.java 100% <100%> (ø) 2 <1> (ø) ⬇️
src/org/javarosa/xform/parse/XFormParser.java 64.51% <36.84%> (-0.79%) 227 <2> (-5)
...g/javarosa/core/reference/PrefixedRootFactory.java 60% <46.15%> (+18.82%) 7 <5> (+3) ⬆️
.../org/javarosa/core/reference/ReferenceManager.java 61.7% <53.33%> (+35.72%) 21 <18> (+10) ⬆️
src/org/javarosa/core/model/FormDef.java 59.78% <75%> (+0.18%) 117 <3> (+3) ⬆️
...rc/org/javarosa/core/reference/RootTranslator.java 43.75% <75%> (-3.31%) 6 <5> (+2)
... and 24 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update aa47832...fb7fbd8. Read the comment docs.

Copy link
Contributor

@ggalmazor ggalmazor left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @dcbriccetti! I think that, overall, this is a really good approach that solves the issue. I'm comfortable with the required change in the API too.

I don't know whether you just wanted to confirm this or you wanted a full head-on review, so I did that as well. I made some comments about some changes I think we need to discuss. Let me know what you thnk! :)

src/org/javarosa/xform/util/ParserCustomizer.java Outdated Show resolved Hide resolved
src/org/javarosa/xform/parse/XFormParser.java Outdated Show resolved Hide resolved
src/org/javarosa/xform/util/XFormUtils.java Outdated Show resolved Hide resolved
test/org/javarosa/xform/parse/XFormParserTest.java Outdated Show resolved Hide resolved
test/org/javarosa/xform/parse/FormParserHelper.java Outdated Show resolved Hide resolved
* class’s corresponding directory
* @return a Path for the resource file
*/
public static Path r(String filename, boolean fallBack) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should throw an error when a test asks for a file that isn't there. In fact, this implementation in masking an error on this PR: the file towns-large.xmlis not present (I'm guessing you forgot to commit the file?), but the timing test still passes.

I like the fallback mechanism though, and I think we should have it as the default behavior, which would avoid some changes on the test classes. I've tried this implementation, which works:

public static Path r(String filename) {
    String resourceFileParentPath = inferResourceFileParentPath();
    Path resourceFilePath = Paths.get("resources", resourceFileParentPath, filename);

    if (Files.exists(resourceFilePath))
        return resourceFilePath;

    Path resources = Paths.get("resources", filename);
    if (Files.exists(resources))
        return resources;

    throw new RuntimeException("File " + filename + " not found");
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I think I'd like to change the implementation to recursively search for the file with Files.walk, but I'm not super crazy about it either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

towns-large.xml is dynamically generated, hence it might not exist when r is called.

Copy link
Contributor

@ggalmazor ggalmazor Jan 2, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, I didn't realize that... sorry!

In any case, what do you think about making it the default behavior and avoid having two parse methods?

This implementation works on all the test scenarios without polluting unknown paths:

public static Path r(String filename) {
    String resourceFileParentPath = inferResourceFileParentPath();
    Path resourceFilePath = Paths.get("resources", resourceFileParentPath, filename);

    if (Files.exists(resourceFilePath))
        return resourceFilePath;

    Path resources = Paths.get("resources", filename);
    if (Files.exists(resources))
        return resources;

    // File doesn't exist. Returning a temp file
    String name = filename.substring(0, filename.lastIndexOf("."));
    String extension = filename.substring(filename.lastIndexOf("."));
    try {
        return Files.createTempFile(name + "-", extension);
    } catch (IOException e) {
        throw new UncheckedIOException(e);
    }
}

This code would create a /tmp/towns-large-8296528288103120809.xml, for example.

@dcbriccetti
Copy link
Contributor Author

I have @cooperka’s forms (see the issue) in Collect, and I’m trying to get them to work. We fail to generate the dynamic options for the second page after pushing a radio button on the first page and swiping to advance. The predicates aren’t matching in XPathEqExpr.eval. I’m digging into why.

@dcbriccetti dcbriccetti removed the request for review from grzesiek2010 December 30, 2018 07:02
@dcbriccetti
Copy link
Contributor Author

OK, that problem is solved. We weren’t setting the instance ID in the ExternalDataInstance’s root nodes. The next problem to solve may be opening a saved form with external data. Here’s a bit of the stack trace I’ll be examining:

12-30 12:00:33.120 21795-21795/org.odk.collect.android E/FormEntryActivity: java.lang.NullPointerException: Attempt to invoke virtual method 'int org.javarosa.core.model.instance.TreeElement.getNumChildren()' on a null object reference
        at org.javarosa.core.model.instance.ExternalDataInstance.getRoot(ExternalDataInstance.java:68)
        at org.javarosa.xpath.expr.XPathPathExprEval.getDataInstance(XPathPathExprEval.java:70)
        at org.javarosa.xpath.expr.XPathPathExprEval.eval(XPathPathExprEval.java:33)
        at org.javarosa.xpath.expr.XPathPathExpr.eval(XPathPathExpr.java:210)
        at org.javarosa.xpath.XPathConditional.evalNodeset(XPathConditional.java:93)
        at org.javarosa.core.model.FormDef.populateDynamicChoices(FormDef.java:993)

@dcbriccetti
Copy link
Contributor Author

OK, I’ve solved that last problem. We weren’t (de)serializing the external instance’s data.

@dcbriccetti
Copy link
Contributor Author

I just realized we probably don’t want to serialize the contents of the external instance. Rather, we should re-parse it upon deserialization of the form it’s in.

ggalmazor and others added 4 commits January 2, 2019 14:23
- The XFormUtils.getFormFromInputStream() method won't let us listen to parse errors/warnings and we need to be able to check them
- Now the RootTranslator won't derive URI that already starts with the translated URI
- Added ResourceManager unit tests
…renceManager to find external instance files.
…form is cached. Log a message and throw an XFormParseException when external instance parsing fails.
@dcbriccetti
Copy link
Contributor Author

That’s the way I first wrote it. But:
/Users/daveb/devel/opendatakit/javarosa/src/org/javarosa/core/reference/ReferenceManager.java:207: error: incompatible types: ArrayList<? extends ReferenceFactory> cannot be converted to List
for (List rfs : Arrays.asList(sessionTranslators, translators, factories)) {

return root;
}
}
for (List<? extends ReferenceFactory> rfs : Arrays.asList(sessionTranslators, translators, factories))
Copy link
Contributor

@ggalmazor ggalmazor Jan 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could do this:

private ReferenceFactory derivingRoot(String uri) throws InvalidReferenceException {
    for (ReferenceFactory rf : getAllReferenceFactoriesInOrder())
        if (rf.derives(uri))
            return rf;

    throw new InvalidReferenceException(getPrettyPrintException(uri), uri);
}

private List<ReferenceFactory> getAllReferenceFactoriesInOrder() {
    List<ReferenceFactory> refFactories = new ArrayList<>();
    refFactories.addAll(sessionTranslators);
    refFactories.addAll(translators);
    refFactories.addAll(factories);
    return refFactories;
}

What do you think?
We could bump the List subtype to a LinkedList to actually enforce order.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

List<? extends ReferenceFactory must really bother you, if you would replace it with that much code. It doesn’t bother me a bit. It’s a list of types that extend ReferenceFactory. I’d rather keep it the way I have it if you don’t feel strongly about it.

Copy link
Contributor

@ggalmazor ggalmazor Jan 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem!

I can't help to not try to fix design problems. Those subtypes are leaking because that hierarchy is not sealed tight. The code I'm suggesting is more verbose but deals with that design flaw. It also flattens the lists, which is kind of nice too.

I won't cry if you don't change it :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for not crying.

Sometime I’d like to understand the design flaw this would deal with.

Copy link
Contributor

@ggalmazor ggalmazor Jan 8, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I don't know why a dagger was showing up in my last comment!)

I agree that my suggestion doesn't really solve the core problem. It's only an aesthetic change. Dealing with the core problem would require deeper work.

This is something super common not only in the ODK codebase, but in many other projects as well, and I think we can blame Java and how Java has been historically teached for that.

The flaw is that the hierarchy formed by ReferenceFactory (supertype) and its subtypes RootTranslator, PrefixedRootFactory, ResourceReferenceFactory, ..., is leaking the subtypes.

You can see this leak in many places. Related to this change we're talking about:

  • You can't change the type of the ReferenceManager translators and sessionTranslators to a List<ReferenceFactory> because it's coupled with a method on RootTranslator.
  • Because of this, you can't directly get a List<List<ReferenceFactory>> as a result of an Array.asList(sessionTranslators, translators, factories).

If you're a SOLID design fan, this is breaking the L (Liskov's Substitution Principle), but in a more general way, this is wrong because it's a misuse of inheritance.

Inheritance is a tool we use to implement polymorphism, and here we don't have polymorphism, which is weird because we could benefit from it (the for block we're discussing would benefit from it).

In a polymorphic type, you can't have the subtypes leaked outside the type hierarchy (no one can be coupled to them, even implicitly, like with the List<? extends ReferenceFactory>).

If this is not a polymorphic type, this is telling me that:

  • Maybe we have used inheritance for code reuse, which would be much simpler to get by composing stuff together
  • Maybe we have used inheritance systematically without taking into account the impact this has in evolving software and the cost of introducing new changes.

This is even worse because the ReferenceFactory is an extension point that clients use to adapt JR to their particular environment. It's really confusing that JR is coupled both to the supertype (OK) and the subtypes (wrong).

Since this is all across the codebase, it's not super critical to fix in this particular instance but you know the Boy Scout's rule: leave the place cleaner than you found it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is very educational!

@ggalmazor
Copy link
Contributor

Even though there are two standing discussions (the one about ResourcePathHelper, and the one about ReferenceFactory), I think this PR is solid work and solves the issue.

I've also tested this in Collect, so I'm confident this is a step in the right direction.

@dcbriccetti, if you feel the same, I'd suggest we merge so that it's easier for you to continue with #397

This includes IDEA-performed changes, a deprecation, and one hand-written optimization.
@dcbriccetti dcbriccetti changed the title WIP: Issue 390 external instances Issue 390 external instances Jan 8, 2019
@dcbriccetti dcbriccetti merged commit 735affc into getodk:master Jan 8, 2019
@dcbriccetti dcbriccetti deleted the issue-390-external-instances branch January 8, 2019 18:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants