diff options
Diffstat (limited to 'src/com')
25 files changed, 1015 insertions, 194 deletions
| diff --git a/src/com/p4square/grow/backend/GrowBackend.java b/src/com/p4square/grow/backend/GrowBackend.java index 533cf09..45e0fa2 100644 --- a/src/com/p4square/grow/backend/GrowBackend.java +++ b/src/com/p4square/grow/backend/GrowBackend.java @@ -15,6 +15,13 @@ import org.restlet.routing.Router;  import com.p4square.grow.config.Config;  import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.backend.db.CassandraKey; +import com.p4square.grow.backend.db.CassandraProviderImpl; + +import com.p4square.grow.model.Question; + +import com.p4square.grow.provider.Provider; +import com.p4square.grow.provider.QuestionProvider;  import com.p4square.grow.backend.resources.AccountResource;  import com.p4square.grow.backend.resources.BannerResource; @@ -29,11 +36,15 @@ import com.p4square.grow.backend.resources.TrainingResource;   * @author Jesse Morgan <jesse@jesterpm.net>   */  public class GrowBackend extends Application { +    private static final String DEFAULT_COLUMN = "value"; +      private final static Logger LOG = Logger.getLogger(GrowBackend.class);      private final Config mConfig;      private final CassandraDatabase mDatabase; +    private final Provider<String, Question> mQuestionProvider; +      public GrowBackend() {          this(new Config());      } @@ -41,6 +52,13 @@ public class GrowBackend extends Application {      public GrowBackend(Config config) {          mConfig = config;          mDatabase = new CassandraDatabase(); + +        mQuestionProvider = new QuestionProvider<CassandraKey>(new CassandraProviderImpl<Question>(mDatabase, "strings", Question.class)) { +            @Override +            public CassandraKey makeKey(String questionId) { +                return new CassandraKey("/questions/" + questionId, DEFAULT_COLUMN); +            } +        };      }      @Override @@ -102,6 +120,10 @@ public class GrowBackend extends Application {          return mDatabase;      } +    public Provider<String, Question> getQuestionProvider() { +        return mQuestionProvider; +    } +      /**       * Stand-alone main for testing.       */ diff --git a/src/com/p4square/grow/backend/db/CassandraKey.java b/src/com/p4square/grow/backend/db/CassandraKey.java new file mode 100644 index 0000000..8e23087 --- /dev/null +++ b/src/com/p4square/grow/backend/db/CassandraKey.java @@ -0,0 +1,28 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +/** + * CassandraKey represents a Cassandra key / column pair. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class CassandraKey { +    private final String mId; +    private final String mColumn; + +    public CassandraKey(String id, String column) { +        mId = id; +        mColumn = column; +    } + +    public String getId() { +        return mId; +    } + +    public String getColumn() { +        return mColumn; +    } +} diff --git a/src/com/p4square/grow/backend/db/CassandraProviderImpl.java b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java new file mode 100644 index 0000000..fb6e34e --- /dev/null +++ b/src/com/p4square/grow/backend/db/CassandraProviderImpl.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.backend.db; + +import java.io.IOException; + +import org.codehaus.jackson.map.DeserializationConfig; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; + +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * Provider implementation backed by a Cassandra ColumnFamily. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class CassandraProviderImpl<V> extends JsonEncodedProvider<CassandraKey, V> { +    private final CassandraDatabase mDb; +    private final String mColumnFamily; + +    public CassandraProviderImpl(CassandraDatabase db, String columnFamily, Class<V> clazz) { +        super(clazz); + +        mDb = db; +        mColumnFamily = columnFamily; +    } + +    @Override +    public V get(CassandraKey key) throws IOException { +        String blob = mDb.getKey(mColumnFamily, key.getId(), key.getColumn()); +        return decode(blob); +    } + +    @Override +    public void put(CassandraKey key, V obj) throws IOException { +        String blob = encode(obj); +        mDb.putKey(mColumnFamily, key.getId(), key.getColumn(), blob); +    } +} diff --git a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java index f0bb2aa..91d4d0f 100644 --- a/src/com/p4square/grow/backend/resources/SurveyResultsResource.java +++ b/src/com/p4square/grow/backend/resources/SurveyResultsResource.java @@ -14,16 +14,20 @@ import org.codehaus.jackson.map.ObjectMapper;  import org.restlet.data.MediaType;  import org.restlet.data.Status; -import org.restlet.resource.ServerResource;  import org.restlet.representation.Representation;  import org.restlet.representation.StringRepresentation; +import org.restlet.resource.ServerResource;  import org.apache.log4j.Logger; -import com.p4square.grow.model.Answer; -import com.p4square.grow.model.Question;  import com.p4square.grow.backend.GrowBackend;  import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.model.Answer; +import com.p4square.grow.model.Question; +import com.p4square.grow.model.RecordedAnswer; +import com.p4square.grow.model.Score; +import com.p4square.grow.provider.Provider; +  /**   * Store the user's answers to the assessment and generate their score. @@ -31,15 +35,16 @@ import com.p4square.grow.backend.db.CassandraDatabase;   * @author Jesse Morgan <jesse@jesterpm.net>   */  public class SurveyResultsResource extends ServerResource { -    private final static Logger cLog = Logger.getLogger(SurveyResultsResource.class); +    private static final Logger LOG = Logger.getLogger(SurveyResultsResource.class); -    private final static ObjectMapper MAPPER = new ObjectMapper(); +    private static final ObjectMapper MAPPER = new ObjectMapper();      static enum RequestType {          ASSESSMENT, ANSWER      }      private CassandraDatabase mDb; +    private Provider<String, Question> mQuestionProvider;      private RequestType mRequestType;      private String mUserId; @@ -51,6 +56,7 @@ public class SurveyResultsResource extends ServerResource {          final GrowBackend backend = (GrowBackend) getApplication();          mDb = backend.getDatabase(); +        mQuestionProvider = backend.getQuestionProvider();          mUserId = getAttribute("userId");          mQuestionId = getAttribute("questionId"); @@ -105,7 +111,7 @@ public class SurveyResultsResource extends ServerResource {                      success = true;                  } catch (Exception e) { -                    cLog.warn("Caught exception putting answer: " + e.getMessage(), e); +                    LOG.warn("Caught exception putting answer: " + e.getMessage(), e);                  }                  break; @@ -146,18 +152,30 @@ public class SurveyResultsResource extends ServerResource {                      continue;                  } -                final String questionId = c.getName(); -                final String answerId   = c.getStringValue(); -                if (!scoringDone) { -                    scoringDone = !scoreQuestion(score, questionId, answerId); +                try { +                    Question question = mQuestionProvider.get(c.getName()); +                    RecordedAnswer userAnswer = MAPPER.readValue(c.getStringValue(), RecordedAnswer.class); + +                    if (question == null) { +                        LOG.warn("Answer for unknown question: " + c.getName()); +                        continue; +                    } + +                    LOG.error("Scoring questionId: " + c.getName()); +                    scoringDone = !question.scoreAnswer(score, userAnswer); + +                } catch (Exception e) { +                    LOG.error("Failed to score question: {userid: \"" + mUserId + +                            "\", questionid:\"" + c.getName() + +                            "\", userAnswer:\"" + c.getStringValue() + "\"}", e);                  }                  totalAnswers++;              } -            sb.append(", \"score\":" + score.sum / score.count); -            sb.append(", \"sum\":" + score.sum); -            sb.append(", \"count\":" + score.count); +            sb.append(", \"score\":" + score.getScore()); +            sb.append(", \"sum\":" + score.getSum()); +            sb.append(", \"count\":" + score.getCount());              sb.append(", \"totalAnswers\":" + totalAnswers);              sb.append(", \"result\":\"" + score.toString() + "\"");          } @@ -170,99 +188,4 @@ public class SurveyResultsResource extends ServerResource {          return summary;      } - -    private boolean scoreQuestion(final Score score, final String questionId, -            final String answerJson) { - -        final String data = mDb.getKey("strings", "/questions/" + questionId); - -        try { -            final Map<?,?> questionMap = MAPPER.readValue(data, Map.class); -            final Map<?,?> answerMap = MAPPER.readValue(answerJson, Map.class); -            final Question question = new Question((Map<String, Object>) questionMap); -            final String answerId = (String) answerMap.get("answerId"); - -            switch (question.getType()) { -                case TEXT: -                case IMAGE: -                    final Answer answer = question.getAnswers().get(answerId); -                    if (answer == null) { -                        cLog.warn("Got unknown answer " + answerId -                                + " for question " + questionId); -                    } else { -                        if (!scoreAnswer(score, answer)) { -                            return false; // Quit scoring -                        } -                    } -                    break; - -                case SLIDER: -                    score.sum += Double.valueOf(answerId) * 4 + 1; -                    score.count++; -                    break; - -                case CIRCLE: -                case QUAD: -                    scoreQuad(score, question, answerId); -                    break; -            } - -        } catch (Exception e) { -            cLog.error("Exception parsing question id " + questionId, e); -        } - -        return true; -    } - -    private boolean scoreAnswer(final Score score, final Answer answer) { -        switch (answer.getType()) { -            case TRUMP: -                score.sum = answer.getScoreFactor(); -                score.count = 1; -                return false; // Quit scoring. - -            case AVERAGE: -                score.sum += answer.getScoreFactor(); -                score.count++; -                break; - -            case NONE: -                break; -        } - -        return true; // Continue scoring -    } - -    private boolean scoreQuad(final Score score, final Question question, -            final String answerId) { - -        Point[] answers = new Point[question.getAnswers().size()]; -        { -            int i = 0; -            for (String answer : question.getAnswers().keySet()) { -               answers[i++] = Point.valueOf(answer); -            } -        } - -        Point userAnswer = Point.valueOf(answerId); - -        double minDistance = Double.MAX_VALUE; -        int answerIndex = 0; -        for (int i = 0; i < answers.length; i++) { -            final double distance = userAnswer.distance(answers[i]); -            if (distance < minDistance) { -                minDistance = distance; -                answerIndex = i; -            } -        } - -        cLog.debug("Quad " + question.getId() + ": Got answer " -                + answers[answerIndex].toString() + " for user point " + answerId); - -        final Answer answer = question.getAnswers().get(answers[answerIndex].toString()); -        score.sum += answer.getScoreFactor(); -        score.count++; - -        return true; // Continue scoring -    }  } diff --git a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java index 009d0fe..6de9507 100644 --- a/src/com/p4square/grow/backend/resources/TrainingRecordResource.java +++ b/src/com/p4square/grow/backend/resources/TrainingRecordResource.java @@ -27,6 +27,8 @@ import org.apache.log4j.Logger;  import com.p4square.grow.backend.GrowBackend;  import com.p4square.grow.backend.db.CassandraDatabase; +import com.p4square.grow.model.Score; +  /**   *   * @author Jesse Morgan <jesse@jesterpm.net> diff --git a/src/com/p4square/grow/frontend/GrowFrontend.java b/src/com/p4square/grow/frontend/GrowFrontend.java index 327554e..6a74bda 100644 --- a/src/com/p4square/grow/frontend/GrowFrontend.java +++ b/src/com/p4square/grow/frontend/GrowFrontend.java @@ -156,10 +156,10 @@ public class GrowFrontend extends FMFacade {          // Static content          try { -            component.getDefaultHost().attach("/images/", new FileServingApp("./build/images/")); -            component.getDefaultHost().attach("/scripts", new FileServingApp("./build/scripts")); -            component.getDefaultHost().attach("/style.css", new FileServingApp("./build/style.css")); -            component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./build/favicon.ico")); +            component.getDefaultHost().attach("/images/", new FileServingApp("./build/root/images/")); +            component.getDefaultHost().attach("/scripts", new FileServingApp("./build/root/scripts")); +            component.getDefaultHost().attach("/style.css", new FileServingApp("./build/root/style.css")); +            component.getDefaultHost().attach("/favicon.ico", new FileServingApp("./build/root/favicon.ico"));          } catch (IOException e) {              LOG.error("Could not create directory for static resources: "                      + e.getMessage(), e); diff --git a/src/com/p4square/grow/frontend/JsonRequestProvider.java b/src/com/p4square/grow/frontend/JsonRequestProvider.java new file mode 100644 index 0000000..c372251 --- /dev/null +++ b/src/com/p4square/grow/frontend/JsonRequestProvider.java @@ -0,0 +1,64 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.frontend; + +import java.io.IOException; + +import org.restlet.Request; +import org.restlet.Response; +import org.restlet.Restlet; +import org.restlet.data.Method; +import org.restlet.data.Status; +import org.restlet.representation.Representation; +import org.restlet.representation.StringRepresentation; + +import com.p4square.grow.provider.JsonEncodedProvider; + +/** + * Fetch a JSON object via a Request. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class JsonRequestProvider<V> extends JsonEncodedProvider<String, V> { + +    private final Restlet mDispatcher; + +    public JsonRequestProvider(Restlet dispatcher, Class<V> clazz) { +        super(clazz); + +        mDispatcher = dispatcher; +    } + +    @Override +    public V get(String url) throws IOException { +        Request request = new Request(Method.GET, url); +        Response response = mDispatcher.handle(request); +        Representation representation = response.getEntity(); + +        if (!response.getStatus().isSuccess()) { +            if (representation != null) { +                representation.release(); +            } + +            throw new IOException("Could not get object. " + response.getStatus()); +        } + +        return decode(representation.getText()); +    } + +    @Override +    public void put(String url, V obj) throws IOException { +        final Request request = new Request(Method.PUT, url); +        request.setEntity(new StringRepresentation(encode(obj))); + +        final Response response = mDispatcher.handle(request); + +        if (!response.getStatus().isSuccess()) { +            throw new IOException("Could not put object. " + response.getStatus()); +        } + +    } + +} diff --git a/src/com/p4square/grow/frontend/SurveyPageResource.java b/src/com/p4square/grow/frontend/SurveyPageResource.java index 415b46c..f864014 100644 --- a/src/com/p4square/grow/frontend/SurveyPageResource.java +++ b/src/com/p4square/grow/frontend/SurveyPageResource.java @@ -4,6 +4,8 @@  package com.p4square.grow.frontend; +import java.io.IOException; +  import java.util.Map;  import java.util.HashMap; @@ -27,6 +29,8 @@ import com.p4square.fmfacade.FreeMarkerPageResource;  import com.p4square.grow.config.Config;  import com.p4square.grow.model.Question; +import com.p4square.grow.provider.QuestionProvider; +import com.p4square.grow.provider.Provider;  /**   * SurveyPageResource handles rendering the survey and processing user's answers. @@ -44,6 +48,7 @@ public class SurveyPageResource extends FreeMarkerPageResource {      private Config mConfig;      private Template mSurveyTemplate;      private JsonRequestClient mJsonClient; +    private Provider<String, Question> mQuestionProvider;      // Fields pertaining to this request.      private String mQuestionId; @@ -62,6 +67,12 @@ public class SurveyPageResource extends FreeMarkerPageResource {          }          mJsonClient = new JsonRequestClient(getContext().getClientDispatcher()); +        mQuestionProvider = new QuestionProvider<String>(new JsonRequestProvider<Question>(getContext().getClientDispatcher(), Question.class)) { +            @Override +            public String makeKey(String questionId) { +                return getBackendEndpoint() + "/assessment/question/" + questionId; +            } +        };          mQuestionId = getAttribute("questionId");          mUserId = getRequest().getClientInfo().getUser().getIdentifier(); @@ -102,7 +113,7 @@ public class SurveyPageResource extends FreeMarkerPageResource {              String selectedAnswer = getAnswer(mQuestionId);              Map root = getRootObject(); -            root.put("question", question.getMap()); +            root.put("question", question);              root.put("selectedAnswerId", selectedAnswer);              // Get the question count and compute progress @@ -214,17 +225,9 @@ public class SurveyPageResource extends FreeMarkerPageResource {      private Question getQuestion(String id) {          try { -            Map<?, ?> questionData = null; - -            JsonResponse response = backendGet("/assessment/question/" + id); -            if (!response.getStatus().isSuccess()) { -                return null; -            } -            questionData = response.getMap(); +            return mQuestionProvider.get(id); -            return new Question((Map<String, Object>) questionData); - -        } catch (ClientException e) { +        } catch (IOException e) {              LOG.warn("Error fetching question.", e);              return null;          } diff --git a/src/com/p4square/grow/model/Answer.java b/src/com/p4square/grow/model/Answer.java index 4c84060..57a1e5d 100644 --- a/src/com/p4square/grow/model/Answer.java +++ b/src/com/p4square/grow/model/Answer.java @@ -4,7 +4,7 @@  package com.p4square.grow.model; -import java.util.Map; +import org.apache.log4j.Logger;  /**   * This is the model of an assessment question's answer. @@ -12,52 +12,131 @@ import java.util.Map;   * @author Jesse Morgan <jesse@jesterpm.net>   */  public class Answer { +    private static final Logger LOG = Logger.getLogger(Answer.class); + +    /** +     * ScoreType determines how the answer will be scored. +     * +     */      public static enum ScoreType { -        NONE, AVERAGE, TRUMP; -    } - -    private final String mAnswerId; -    private final String mAnswerText; -    private final ScoreType mType; -    private final float mScoreFactor; -    private final String mNextQuestionId; - -    public Answer(final String id, final Map<String, Object> answer) { -        mAnswerId = id; -        mAnswerText = (String) answer.get("text"); -        final String typeStr = (String) answer.get("type"); -        if (typeStr == null) { -            mType = ScoreType.AVERAGE; -        } else { -            mType = ScoreType.valueOf(typeStr.toUpperCase()); -        } +        /** +         * This question has no effect on the score. +         */ +        NONE, -        if (mType != ScoreType.NONE) { -            mScoreFactor = Float.valueOf((String) answer.get("score")); -        } else { -            mScoreFactor = 0; -        } +        /** +         * The score of this question is part of the average. +         */ +        AVERAGE, -        mNextQuestionId = (String) answer.get("nextQuestion"); +        /** +         * The score of this question is the total score, no other questions +         * matter after this point. +         */ +        TRUMP; + +        @Override +        public String toString() { +            return name().toLowerCase(); +        }      } -    public String getId() { -        return mAnswerId; +    private String mAnswerText; +    private ScoreType mType; +    private float mScoreFactor; +    private String mNextQuestionId; + +    public Answer() { +        mType = ScoreType.AVERAGE;      } +    /** +     * @return The text associated with the answer. +     */      public String getText() {          return mAnswerText;      } +    /** +     * Set the text associated with the answer. +     * @param text The new text. +     */ +    public void setText(String text) { +        mAnswerText = text; +    } + +    /** +     * @return the ScoreType for the Answer. +     */      public ScoreType getType() {          return mType;      } -    public float getScoreFactor() { +    /** +     * Set the ScoreType for the answer. +     * @param type The new ScoreType. +     */ +    public void setType(ScoreType type) { +        mType = type; +    } + +    /** +     * @return the delta of the score if this answer is selected. +     */ +    public float getScore() { +        if (mType == ScoreType.NONE) { +            return 0; +        } +          return mScoreFactor;      } +    /** +     * Set the score delta for this answer. +     * @param score The new delta. +     */ +    public void setScore(float score) { +        mScoreFactor = score; +    } + +    /** +     * @return the id of the next question if this answer is selected, or null +     *         if selecting this answer has no effect. +     */      public String getNextQuestion() {          return mNextQuestionId;      } + +    /** +     * Set the id of the next question when this answer is selected. +     * @param id The next question id or null to proceed as usual. +     */ +    public void setNextQuestion(String id) { +        mNextQuestionId = id; +    } + +    /** +     * Adjust the running score for the selection of this answer. +     * @param score The running score to adjust. +     * @return true if scoring should continue, false if this answer trumps all. +     */ +    public boolean score(final Score score) { +        switch (getType()) { +            case TRUMP: +                score.sum = getScore(); +                score.count = 1; +                return false; // Quit scoring. + +            case AVERAGE: +                LOG.error("ScoreType.AVERAGE: { delta: \"" + getScore() + "\" }"); +                score.sum += getScore(); +                score.count++; +                break; + +            case NONE: +                break; +        } + +        return true; // Continue scoring +    }  } diff --git a/src/com/p4square/grow/model/CircleQuestion.java b/src/com/p4square/grow/model/CircleQuestion.java new file mode 100644 index 0000000..71acc14 --- /dev/null +++ b/src/com/p4square/grow/model/CircleQuestion.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Circle Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class CircleQuestion extends Question { +    private static final ScoringEngine ENGINE = new QuadScoringEngine(); + +    private String mTopLeft; +    private String mTopRight; +    private String mBottomLeft; +    private String mBottomRight; + +    /** +     * @return the Top Left label. +     */ +    public String getTopLeft() { +        return mTopLeft; +    } + +    /** +     * Set the Top Left label. +     * @param s The new top left label. +     */ +    public void setTopLeft(String s) { +        mTopLeft = s; +    } + +    /** +     * @return the Top Right label. +     */ +    public String getTopRight() { +        return mTopRight; +    } + +    /** +     * Set the Top Right label. +     * @param s The new top left label. +     */ +    public void setTopRight(String s) { +        mTopRight = s; +    } + +    /** +     * @return the Bottom Left label. +     */ +    public String getBottomLeft() { +        return mBottomLeft; +    } + +    /** +     * Set the Bottom Left label. +     * @param s The new top left label. +     */ +    public void setBottomLeft(String s) { +        mBottomLeft = s; +    } + +    /** +     * @return the Bottom Right label. +     */ +    public String getBottomRight() { +        return mBottomRight; +    } + +    /** +     * Set the Bottom Right label. +     * @param s The new top left label. +     */ +    public void setBottomRight(String s) { +        mBottomRight = s; +    } + +    @Override +    public boolean scoreAnswer(Score score, RecordedAnswer answer) { +        return ENGINE.scoreAnswer(score, this, answer); +    } + +    @Override +    public QuestionType getType() { +        return QuestionType.CIRCLE; +    } +} diff --git a/src/com/p4square/grow/model/ImageQuestion.java b/src/com/p4square/grow/model/ImageQuestion.java new file mode 100644 index 0000000..d94c32c --- /dev/null +++ b/src/com/p4square/grow/model/ImageQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Image Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class ImageQuestion extends Question { +    private static final ScoringEngine ENGINE = new SimpleScoringEngine(); + +    @Override +    public boolean scoreAnswer(Score score, RecordedAnswer answer) { +        return ENGINE.scoreAnswer(score, this, answer); +    } + +    @Override +    public QuestionType getType() { +        return QuestionType.IMAGE; +    } +} diff --git a/src/com/p4square/grow/backend/resources/Point.java b/src/com/p4square/grow/model/Point.java index e1b15a8..e9fc0ca 100644 --- a/src/com/p4square/grow/backend/resources/Point.java +++ b/src/com/p4square/grow/model/Point.java @@ -2,17 +2,23 @@   * Copyright 2013 Jesse Morgan   */ -package com.p4square.grow.backend.resources; +package com.p4square.grow.model;  /**   * Simple double based point class.   *   * @author Jesse Morgan <jesse@jesterpm.net>   */ -class Point { +public class Point { +    /** +     * Parse a comma separated x,y pair into a point. +     * +     * @return The point represented by the string. +     * @throws IllegalArgumentException if the input is malformed. +     */      public static Point valueOf(String str) {          final int comma = str.indexOf(','); -        if (comma == -1) { +        if (comma == -1 || comma == 0 || comma == str.length() - 1) {              throw new IllegalArgumentException("Malformed point string");          } @@ -20,16 +26,28 @@ class Point {          final String sY = str.substring(comma + 1);          return new Point(Double.valueOf(sX), Double.valueOf(sY)); -    }      +    }      private final double mX;      private final double mY; +    /** +     * Create a new point with the given coordinates. +     * +     * @param x The x coordinate. +     * @param y The y coordinate. +     */      public Point(double x, double y) {          mX = x;          mY = y;      } +    /** +     * Compute the distance between this point and another. +     * +     * @param other The other point. +     * @return The distance between this point and other. +     */      public double distance(Point other) {          final double dx = mX - other.mX;          final double dy = mY - other.mY; @@ -37,14 +55,23 @@ class Point {          return Math.sqrt(dx*dx + dy*dy);      } +    /** +     * @return The x coordinate. +     */      public double getX() {          return mX;      } +    /** +     * @return The y coordinate. +     */      public double getY() {          return mY;      } +    /** +     * @return The point represented as a comma separated pair. +     */      @Override      public String toString() {          return String.format("%.2f,%.2f", mX, mY); diff --git a/src/com/p4square/grow/model/QuadQuestion.java b/src/com/p4square/grow/model/QuadQuestion.java new file mode 100644 index 0000000..a7b4179 --- /dev/null +++ b/src/com/p4square/grow/model/QuadQuestion.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Two-dimensional Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class QuadQuestion extends Question { +    private static final ScoringEngine ENGINE = new QuadScoringEngine(); + +    private String mTop; +    private String mRight; +    private String mBottom; +    private String mLeft; + +    /** +     * @return the top label. +     */ +    public String getTop() { +        return mTop; +    } + +    /** +     * Set the top label. +     * @param s The new top label. +     */ +    public void setTop(String s) { +        mTop = s; +    } + +    /** +     * @return the right label. +     */ +    public String getRight() { +        return mRight; +    } + +    /** +     * Set the right label. +     * @param s The new right label. +     */ +    public void setRight(String s) { +        mRight = s; +    } + +    /** +     * @return the bottom label. +     */ +    public String getBottom() { +        return mBottom; +    } + +    /** +     * Set the bottom label. +     * @param s The new bottom label. +     */ +    public void setBottom(String s) { +        mBottom = s; +    } + +    /** +     * @return the left label. +     */ +    public String getLeft() { +        return mLeft; +    } + +    /** +     * Set the left label. +     * @param s The new left label. +     */ +    public void setLeft(String s) { +        mLeft = s; +    } + +    @Override +    public boolean scoreAnswer(Score score, RecordedAnswer answer) { +        return ENGINE.scoreAnswer(score, this, answer); +    } + +    @Override +    public QuestionType getType() { +        return QuestionType.QUAD; +    } +} diff --git a/src/com/p4square/grow/model/QuadScoringEngine.java b/src/com/p4square/grow/model/QuadScoringEngine.java new file mode 100644 index 0000000..33403b5 --- /dev/null +++ b/src/com/p4square/grow/model/QuadScoringEngine.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import com.p4square.grow.model.Point; + +/** + * QuadScoringEngine expects the user's answer to be a Point string. We find + * the closest answer Point to the user's answer and treat that as the answer. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class QuadScoringEngine extends ScoringEngine { + +    @Override +    public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { +        // Find all of the answer points. +        Point[] answers = new Point[question.getAnswers().size()]; +        { +            int i = 0; +            for (String answerStr : question.getAnswers().keySet()) { +               answers[i++] = Point.valueOf(answerStr); +            } +        } + +        // Parse the user's answer. +        Point userPoint = Point.valueOf(userAnswer.getAnswerId()); + +        // Find the closest answer point to the user's answer. +        double minDistance = Double.MAX_VALUE; +        int answerIndex = 0; +        for (int i = 0; i < answers.length; i++) { +            final double distance = userPoint.distance(answers[i]); +            if (distance < minDistance) { +                minDistance = distance; +                answerIndex = i; +            } +        } + +        LOG.debug("Quad " + question.getId() + ": Got answer " +                + answers[answerIndex].toString() + " for user point " + userAnswer); + +        // Get the answer and update the score. +        final Answer answer = question.getAnswers().get(answers[answerIndex].toString()); +        return answer.score(score); +    } +} diff --git a/src/com/p4square/grow/model/Question.java b/src/com/p4square/grow/model/Question.java index 387d723..37deffa 100644 --- a/src/com/p4square/grow/model/Question.java +++ b/src/com/p4square/grow/model/Question.java @@ -4,76 +4,125 @@  package com.p4square.grow.model; -import java.util.Collections;  import java.util.HashMap;  import java.util.Map; +import org.codehaus.jackson.annotate.JsonSubTypes; +import org.codehaus.jackson.annotate.JsonSubTypes.Type; +import org.codehaus.jackson.annotate.JsonTypeInfo; +  /**   * Model of an assessment question.   *   * @author Jesse Morgan <jesse@jesterpm.net>   */ -public class Question { -    public static enum QuestionType { -        TEXT, IMAGE, SLIDER, QUAD, CIRCLE; +@JsonTypeInfo( +    use = JsonTypeInfo.Id.NAME, +    include = JsonTypeInfo.As.PROPERTY, +    property = "type") +@JsonSubTypes({ +    @Type(value = TextQuestion.class, name = "text"), +    @Type(value = ImageQuestion.class, name = "image"), +    @Type(value = SliderQuestion.class, name = "slider"), +    @Type(value = QuadQuestion.class, name = "quad"), +    @Type(value = CircleQuestion.class, name = "circle"), +}) +public abstract class Question { +    /** +     * QuestionType indicates the type of Question. +     * +     * @author Jesse Morgan <jesse@jesterpm.net> +     */ +    public enum QuestionType { +        TEXT, +        IMAGE, +        SLIDER, +        QUAD, +        CIRCLE; + +        @Override +        public String toString() { +            return name().toLowerCase(); +        }      } -    private final Map<String, Object> mMap; -    private final String mQuestionId; -    private final QuestionType mType; -    private final String mQuestionText; +    private String mQuestionId; +    private QuestionType mType; +    private String mQuestionText;      private Map<String, Answer> mAnswers; -    private final String mPreviousQuestionId; -    private final String mNextQuestionId; - -    public Question(final Map<String, Object> map) { -        mMap = map; -        mQuestionId = (String) map.get("id"); -        mType = QuestionType.valueOf(((String) map.get("type")).toUpperCase()); - -        mQuestionText = (String) map.get("text"); - -        mPreviousQuestionId = (String) map.get("previousQuestion"); -        mNextQuestionId = (String) map.get("nextQuestion"); +    private String mPreviousQuestionId; +    private String mNextQuestionId; +    public Question() {          mAnswers = new HashMap<String, Answer>(); -        for (Map.Entry<String, Object> answer : -                ((Map<String, Object>) map.get("answers")).entrySet()) { - -            final String id = answer.getKey(); -            final Map<String, Object> answerMap = (Map<String, Object>) answer.getValue(); -            final Answer answerObj = new Answer(id, answerMap); -            mAnswers.put(id, answerObj); -        }      } +    /** +     * @return the id String for this question. +     */      public String getId() {          return mQuestionId;      } -    public QuestionType getType() { -        return mType; +    /** +     * Set the id String for this question. +     * @param id New id +     */ +    public void setId(String id) { +        mQuestionId = id;      } -    public String getText() { +    /** +     * @return The Question text. +     */ +    public String getQuestion() {          return mQuestionText;      } +    /** +     * Set the question text. +     * @param value The new question text. +     */ +    public void setQuestion(String value) { +        mQuestionText = value; +    } + +    /** +     * @return The id String of the previous question or null if no previous question exists. +     */      public String getPreviousQuestion() {          return mPreviousQuestionId;      } +    /** +     * Set the id string of the previous question. +     * @param id Previous question id or null if there is no previous question. +     */ +    public void setPreviousQuestion(String id) { +        mPreviousQuestionId = id; +    } + +    /** +     * @return The id String of the next question or null if no next question exists. +     */      public String getNextQuestion() {          return mNextQuestionId;      } -    public Map<String, Answer> getAnswers() { -        return Collections.unmodifiableMap(mAnswers); +    /** +     * Set the id string of the next question. +     * @param id next question id or null if there is no next question. +     */ +    public void setNextQuestion(String id) { +        mNextQuestionId = id;      } -    public Map<String, Object> getMap() { -        return Collections.unmodifiableMap(mMap); +    /** +     * @return a map of Answer id Strings to Answer objects. +     */ +    public Map<String, Answer> getAnswers() { +        return mAnswers;      }      /** @@ -98,4 +147,19 @@ public class Question {          return nextQuestion;      } + +    /** +     * Update the score based on the answer to this question. +     * +     * @param score The running score to update. +     * @param answer The answer give to this question. +     * @return true if scoring should continue, false if this answer trumps everything else. +     */ +    public abstract boolean scoreAnswer(Score score, RecordedAnswer answer); + +    /** +     * @return the QuestionType of this question. +     */ +    public abstract QuestionType getType(); +  } diff --git a/src/com/p4square/grow/model/RecordedAnswer.java b/src/com/p4square/grow/model/RecordedAnswer.java new file mode 100644 index 0000000..7d9905d --- /dev/null +++ b/src/com/p4square/grow/model/RecordedAnswer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple model for a user's assessment answer. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class RecordedAnswer { +    private String mAnswerId; + +    /** +     * @return The user's answer. +     */ +    public String getAnswerId() { +        return mAnswerId; +    } + +    /** +     * Set the answer id field. +     * @param id The new id. +     */ +    public void setAnswerId(String id) { +        mAnswerId = id; +    } + +    @Override +    public String toString() { +        return mAnswerId; +    } +} diff --git a/src/com/p4square/grow/backend/resources/Score.java b/src/com/p4square/grow/model/Score.java index 6f52c02..b6deda2 100644 --- a/src/com/p4square/grow/backend/resources/Score.java +++ b/src/com/p4square/grow/model/Score.java @@ -2,14 +2,14 @@   * Copyright 2013 Jesse Morgan   */ -package com.p4square.grow.backend.resources; +package com.p4square.grow.model;  /**   * Simple structure containing a score's sum and count.   *   * @author Jesse Morgan <jesse@jesterpm.net>   */ -class Score { +public class Score {      /**       * Return the integer value for the given Score String.       */ @@ -28,9 +28,30 @@ class Score {      double sum;      int count; +    /** +     * @return The sum of all the points. +     */ +    public double getSum() { +        return sum; +    } + +    /** +     * @return The number of questions included in the score. +     */ +    public int getCount() { +        return count; +    } + +    /** +     * @return The final score. +     */ +    public double getScore() { +        return sum / count; +    } +      @Override      public String toString() { -        final double score = sum / count; +        final double score = getScore();          if (score >= 4) {              return "teacher"; @@ -45,5 +66,5 @@ class Score {              return "seeker";          }      } -      +  } diff --git a/src/com/p4square/grow/model/ScoringEngine.java b/src/com/p4square/grow/model/ScoringEngine.java new file mode 100644 index 0000000..8ff18b3 --- /dev/null +++ b/src/com/p4square/grow/model/ScoringEngine.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.apache.log4j.Logger; + +/** + * ScoringEngine computes the score for a question and a given answer. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public abstract class ScoringEngine { +    protected static final Logger LOG = Logger.getLogger(ScoringEngine.class); + +    /** +     * Update the score based on the given question and answer. +     * +     * @param score The running score to update. +     * @param question The question to compute the score for. +     * @param answer The answer give to this question. +     * @return true if scoring should continue, false if this answer trumps everything else. +     */ +    public abstract boolean scoreAnswer(Score score, Question question, RecordedAnswer answer); +} diff --git a/src/com/p4square/grow/model/SimpleScoringEngine.java b/src/com/p4square/grow/model/SimpleScoringEngine.java new file mode 100644 index 0000000..6ef2dbb --- /dev/null +++ b/src/com/p4square/grow/model/SimpleScoringEngine.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * SimpleScoringEngine expects the user's answer to a valid answer id and + * scores accordingly. + * + * If the answer id is not valid an Exception is thrown. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SimpleScoringEngine extends ScoringEngine { + +    @Override +    public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { +        final Answer answer = question.getAnswers().get(userAnswer.getAnswerId()); +        if (answer == null) { +            throw new IllegalArgumentException("Not a valid answer."); +        } + +        return answer.score(score); +    } +} diff --git a/src/com/p4square/grow/model/SliderQuestion.java b/src/com/p4square/grow/model/SliderQuestion.java new file mode 100644 index 0000000..f0861e3 --- /dev/null +++ b/src/com/p4square/grow/model/SliderQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Slider Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SliderQuestion extends Question { +    private static final ScoringEngine ENGINE = new SliderScoringEngine(); + +    @Override +    public boolean scoreAnswer(Score score, RecordedAnswer answer) { +        return ENGINE.scoreAnswer(score, this, answer); +    } + +    @Override +    public QuestionType getType() { +        return QuestionType.SLIDER; +    } +} diff --git a/src/com/p4square/grow/model/SliderScoringEngine.java b/src/com/p4square/grow/model/SliderScoringEngine.java new file mode 100644 index 0000000..76811b3 --- /dev/null +++ b/src/com/p4square/grow/model/SliderScoringEngine.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * SliderScoringEngine expects the user's answer to be a decimal value in the + * range [0, 1]. The value is scaled to the range [1, 4] and added to the + * score. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SliderScoringEngine extends ScoringEngine { + +    @Override +    public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { +        float delta = Float.valueOf(userAnswer.getAnswerId()) * 3 + 1; + +        if (delta < 0 || delta > 4) { +            throw new IllegalArgumentException("Answer out of bounds."); +        } + +        score.sum += delta; +        score.count++; + +        return true; +    } +} diff --git a/src/com/p4square/grow/model/TextQuestion.java b/src/com/p4square/grow/model/TextQuestion.java new file mode 100644 index 0000000..88c2a34 --- /dev/null +++ b/src/com/p4square/grow/model/TextQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Text Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class TextQuestion extends Question { +    private static final ScoringEngine ENGINE = new SimpleScoringEngine(); + +    @Override +    public boolean scoreAnswer(Score score, RecordedAnswer answer) { +        return ENGINE.scoreAnswer(score, this, answer); +    } + +    @Override +    public QuestionType getType() { +        return QuestionType.TEXT; +    } +} diff --git a/src/com/p4square/grow/provider/JsonEncodedProvider.java b/src/com/p4square/grow/provider/JsonEncodedProvider.java new file mode 100644 index 0000000..605b18c --- /dev/null +++ b/src/com/p4square/grow/provider/JsonEncodedProvider.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +import org.codehaus.jackson.map.DeserializationConfig; +import org.codehaus.jackson.map.ObjectMapper; +import org.codehaus.jackson.map.SerializationConfig; + +/** + * Provider provides a simple interface for loading and persisting + * objects. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public abstract class JsonEncodedProvider<K, V> implements Provider<K, V> { +    public static final ObjectMapper MAPPER = new ObjectMapper(); +    static { +        MAPPER.configure(SerializationConfig.Feature.WRITE_ENUMS_USING_TO_STRING, true); +        MAPPER.configure(DeserializationConfig.Feature.READ_ENUMS_USING_TO_STRING, true); +        MAPPER.configure(DeserializationConfig.Feature.FAIL_ON_UNKNOWN_PROPERTIES, false); +    } + +    private final Class<V> mClazz; + +    public JsonEncodedProvider(Class<V> clazz) { +        mClazz = clazz; +    } + +    /** +     * Encode the object as JSON. +     * +     * @param obj The object to encode. +     * @return The JSON encoding of obj. +     * @throws IOException if the object cannot be encoded. +     */ +    protected String encode(V obj) throws IOException { +        return MAPPER.writeValueAsString(obj); +    } + +    /** +     * Decode the JSON string as an object. +     * +     * @param blob The JSON data to decode. +     * @return The decoded object or null if blob is null. +     * @throws IOException If an object cannot be decoded. +     */ +    protected V decode(String blob) throws IOException { +        if (blob == null) { +            return null; +        } + +        V obj = MAPPER.readValue(blob, mClazz); +        return obj; +    } +} + diff --git a/src/com/p4square/grow/provider/Provider.java b/src/com/p4square/grow/provider/Provider.java new file mode 100644 index 0000000..ca6af25 --- /dev/null +++ b/src/com/p4square/grow/provider/Provider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +/** + * Provider provides a simple interface for loading and persisting + * objects. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public interface Provider<K, V> { +    /** +     * Retrieve the object with the given key. +     * +     * @param key The key for the object. +     * @return The object or null if not found. +     */ +    V get(K key) throws IOException; + +    /** +     * Persist the object with the given key. +     * +     * @param key The key for the object. +     * @param obj The object to persist. +     */ +    void put(K key, V obj) throws IOException; +} diff --git a/src/com/p4square/grow/provider/QuestionProvider.java b/src/com/p4square/grow/provider/QuestionProvider.java new file mode 100644 index 0000000..b569dc8 --- /dev/null +++ b/src/com/p4square/grow/provider/QuestionProvider.java @@ -0,0 +1,41 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.provider; + +import java.io.IOException; + +import com.p4square.grow.model.Question; + +/** + * QuestionProvider wraps an existing Provider to get and put Questions. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public abstract class QuestionProvider<K> implements Provider<String, Question> { + +    private Provider<K, Question> mProvider; + +    public QuestionProvider(Provider<K, Question> provider) { +        mProvider = provider; +    } + +    @Override +    public Question get(String key) throws IOException { +        return mProvider.get(makeKey(key)); +    } + +    @Override +    public void put(String key, Question obj) throws IOException { +        mProvider.put(makeKey(key), obj); +    } + +    /** +     * Make a Key for questionId. +     * +     * @param questionId The question id. +     * @return a key for questionId. +     */ +    protected abstract K makeKey(String questionId); +} | 
