From 55cba1e0f3373fa69d3b9a66f455ad36ab4b82cf Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Sun, 20 Mar 2016 17:07:26 -0700 Subject: Adding support for Church Community Builder login. Beginning with this change all of the Church Management System integration logic is moving into implementations of the new IntegrationDriver interface. The desired IntegrationDriver can be selected by setting the integrationDriver config to the appropriate class name. This commit is only moving login support. Progress reporting will move in a later commit. --- .../grow/frontend/AssessmentResultsPage.java | 4 ++- .../grow/frontend/ChapterCompletePage.java | 4 ++- src/com/p4square/grow/frontend/GrowFrontend.java | 39 +++++++++++----------- .../p4square/grow/frontend/IntegrationDriver.java | 17 ++++++++++ .../p4square/grow/frontend/NewAccountResource.java | 16 +++++++-- 5 files changed, 57 insertions(+), 23 deletions(-) create mode 100644 src/com/p4square/grow/frontend/IntegrationDriver.java (limited to 'src/com/p4square/grow/frontend') diff --git a/src/com/p4square/grow/frontend/AssessmentResultsPage.java b/src/com/p4square/grow/frontend/AssessmentResultsPage.java index 2035ce8..9b66794 100644 --- a/src/com/p4square/grow/frontend/AssessmentResultsPage.java +++ b/src/com/p4square/grow/frontend/AssessmentResultsPage.java @@ -7,6 +7,7 @@ package com.p4square.grow.frontend; import java.util.Date; import java.util.Map; +import com.p4square.f1oauth.FellowshipOneIntegrationDriver; import freemarker.template.Template; import org.restlet.data.MediaType; @@ -117,7 +118,8 @@ public class AssessmentResultsPage extends FreeMarkerPageResource { attribute.setStartDate(new Date()); attribute.setComment(JsonEncodedProvider.MAPPER.writeValueAsString(results)); - F1API f1 = mGrowFrontend.getF1Access().getAuthenticatedApi(user); + F1API f1 = ((FellowshipOneIntegrationDriver) mGrowFrontend.getThirdPartyIntegrationFactory()) + .getF1Access().getAuthenticatedApi(user); if (!f1.addAttribute(user.getIdentifier(), attribute)) { LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute " + attributeName); diff --git a/src/com/p4square/grow/frontend/ChapterCompletePage.java b/src/com/p4square/grow/frontend/ChapterCompletePage.java index f07a870..2dd1ecf 100644 --- a/src/com/p4square/grow/frontend/ChapterCompletePage.java +++ b/src/com/p4square/grow/frontend/ChapterCompletePage.java @@ -7,6 +7,7 @@ package com.p4square.grow.frontend; import java.util.Date; import java.util.Map; +import com.p4square.f1oauth.FellowshipOneIntegrationDriver; import freemarker.template.Template; import org.restlet.data.MediaType; @@ -172,7 +173,8 @@ public class ChapterCompletePage extends FreeMarkerPageResource { Attribute attribute = new Attribute(attributeName); attribute.setStartDate(new Date()); - F1API f1 = mGrowFrontend.getF1Access().getAuthenticatedApi(user); + F1API f1 = ((FellowshipOneIntegrationDriver) mGrowFrontend.getThirdPartyIntegrationFactory()) + .getF1Access().getAuthenticatedApi(user); if (!f1.addAttribute(user.getIdentifier(), attribute)) { LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute " + attributeName); diff --git a/src/com/p4square/grow/frontend/GrowFrontend.java b/src/com/p4square/grow/frontend/GrowFrontend.java index 1d221cc..b5f62fb 100644 --- a/src/com/p4square/grow/frontend/GrowFrontend.java +++ b/src/com/p4square/grow/frontend/GrowFrontend.java @@ -6,15 +6,12 @@ package com.p4square.grow.frontend; import java.io.File; import java.io.IOException; - -import java.util.Arrays; -import java.util.UUID; +import java.lang.reflect.Constructor; import freemarker.template.Template; import org.restlet.Application; import org.restlet.Component; -import org.restlet.Client; import org.restlet.Context; import org.restlet.Restlet; import org.restlet.data.Protocol; @@ -32,13 +29,11 @@ import com.p4square.fmfacade.FreeMarkerPageResource; import com.p4square.grow.config.Config; -import com.p4square.f1oauth.F1Access; -import com.p4square.f1oauth.SecondPartyVerifier; - import com.p4square.restlet.metrics.MetricRouter; import com.p4square.session.SessionCheckingAuthenticator; import com.p4square.session.SessionCreatingAuthenticator; +import org.restlet.security.Verifier; /** * This is the Restlet Application implementing the Grow project front-end. @@ -54,7 +49,7 @@ public class GrowFrontend extends FMFacade { private final Config mConfig; private final MetricRegistry mMetricRegistry; - private F1Access mHelper; + private IntegrationDriver mIntegrationFactory; public GrowFrontend() { this(new Config(), new MetricRegistry()); @@ -81,20 +76,26 @@ public class GrowFrontend extends FMFacade { FreeMarkerPageResource.baseRootObject(getContext(), this)); } + getContext().getAttributes().put("com.p4square.grow.config", mConfig); + getContext().getAttributes().put("com.p4square.grow.metrics", mMetricRegistry); + super.start(); } - synchronized F1Access getF1Access() { - if (mHelper == null) { - mHelper = new F1Access(getContext(), mConfig.getString("f1ConsumerKey", ""), - mConfig.getString("f1ConsumerSecret", ""), - mConfig.getString("f1BaseUrl", "staging.fellowshiponeapi.com"), - mConfig.getString("f1ChurchCode", "pfseawa"), - F1Access.UserType.WEBLINK); - mHelper.setMetricRegistry(mMetricRegistry); + public synchronized IntegrationDriver getThirdPartyIntegrationFactory() { + if (mIntegrationFactory == null) { + final String driverClassName = getConfig().getString("integrationDriver", + "com.p4square.f1oauth.FellowshipOneIntegrationDriver"); + try { + Class clazz = Class.forName(driverClassName); + Constructor constructor = clazz.getConstructor(Context.class); + mIntegrationFactory = (IntegrationDriver) constructor.newInstance(getContext()); + } catch (Exception e) { + LOG.error("Failed to instantiate IntegrationDriver " + driverClassName); + } } - return mHelper; + return mIntegrationFactory; } @Override @@ -141,8 +142,8 @@ public class GrowFrontend extends FMFacade { SessionCheckingAuthenticator sessionChk = new SessionCheckingAuthenticator(context, true); // This is used to authenticate the user - SecondPartyVerifier f1Verifier = new SecondPartyVerifier(context, getF1Access()); - LoginFormAuthenticator loginAuth = new LoginFormAuthenticator(context, false, f1Verifier); + Verifier verifier = getThirdPartyIntegrationFactory().newUserAuthenticationVerifier(); + LoginFormAuthenticator loginAuth = new LoginFormAuthenticator(context, false, verifier); loginAuth.setLoginFormUrl(loginPage); loginAuth.setLoginPostUrl(loginPost); loginAuth.setDefaultPage(defaultPage); diff --git a/src/com/p4square/grow/frontend/IntegrationDriver.java b/src/com/p4square/grow/frontend/IntegrationDriver.java new file mode 100644 index 0000000..3370116 --- /dev/null +++ b/src/com/p4square/grow/frontend/IntegrationDriver.java @@ -0,0 +1,17 @@ +package com.p4square.grow.frontend; + +import org.restlet.security.Verifier; + +/** + * An IntegrationDriver is used to create implementations of various objects + * used to integration Grow with a particular Church Management System. + */ +public interface IntegrationDriver { + + /** + * Create a new Restlet Verifier to authenticate users when they login to the site. + * + * @return A Verifier. + */ + Verifier newUserAuthenticationVerifier(); +} diff --git a/src/com/p4square/grow/frontend/NewAccountResource.java b/src/com/p4square/grow/frontend/NewAccountResource.java index 54c1790..5c13017 100644 --- a/src/com/p4square/grow/frontend/NewAccountResource.java +++ b/src/com/p4square/grow/frontend/NewAccountResource.java @@ -6,12 +6,12 @@ package com.p4square.grow.frontend; import java.util.Map; +import com.p4square.f1oauth.FellowshipOneIntegrationDriver; import freemarker.template.Template; import org.restlet.data.Form; 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.ext.freemarker.TemplateRepresentation; @@ -44,7 +44,14 @@ public class NewAccountResource extends FreeMarkerPageResource { super.doInit(); mGrowFrontend = (GrowFrontend) getApplication(); - mHelper = mGrowFrontend.getF1Access(); + + final IntegrationDriver driver = mGrowFrontend.getThirdPartyIntegrationFactory(); + if (driver instanceof FellowshipOneIntegrationDriver) { + mHelper = ((FellowshipOneIntegrationDriver) driver).getF1Access(); + } else { + LOG.error("NewAccountResource only works with F1!"); + mHelper = null; + } mErrorMessage = ""; @@ -83,6 +90,11 @@ public class NewAccountResource extends FreeMarkerPageResource { @Override protected Representation post(Representation rep) { + if (mHelper == null) { + mErrorMessage += "F1 support is not enabled! "; + return get(); + } + Form form = new Form(rep); String firstname = form.getFirstValue("firstname"); -- cgit v1.2.3 From ab52d26e4c0e3d939e681020c7c2cc3d14d4b595 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Mon, 21 Mar 2016 22:16:52 -0700 Subject: Introducing the ProgressReporter interface. A ProgressReporter will be used to record progress in the ChMS. The F1 version in complete, but needs to be plugged into AssessmentResultPage and ChapterCompletePage. --- .gitignore | 3 ++ src/com/p4square/f1oauth/F1ProgressReporter.java | 57 ++++++++++++++++++++++ .../p4square/grow/frontend/ProgressReporter.java | 29 +++++++++++ 3 files changed, 89 insertions(+) create mode 100644 src/com/p4square/f1oauth/F1ProgressReporter.java create mode 100644 src/com/p4square/grow/frontend/ProgressReporter.java (limited to 'src/com/p4square/grow/frontend') diff --git a/.gitignore b/.gitignore index 4002c41..7e32fa1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ build +out/ lib *.swp .idea +*.iml +devfiles/grow-server.properties diff --git a/src/com/p4square/f1oauth/F1ProgressReporter.java b/src/com/p4square/f1oauth/F1ProgressReporter.java new file mode 100644 index 0000000..8382020 --- /dev/null +++ b/src/com/p4square/f1oauth/F1ProgressReporter.java @@ -0,0 +1,57 @@ +package com.p4square.f1oauth; + +import com.p4square.grow.frontend.ProgressReporter; +import org.apache.log4j.Logger; +import org.restlet.security.User; + +import java.util.Date; + +/** + * A ProgressReporter implementation to record progress in F1. + */ +public class F1ProgressReporter implements ProgressReporter { + + private static final Logger LOG = Logger.getLogger(F1ProgressReporter.class); + + private F1Access mF1Access; + + public F1ProgressReporter(final F1Access f1access) { + mF1Access = f1access; + } + + @Override + public void reportAssessmentComplete(final User user, final String level, final Date date, final String results) { + String attributeName = "Assessment Complete - " + level; + Attribute attribute = new Attribute(attributeName); + attribute.setStartDate(date); + attribute.setComment(results); + addAttribute(user, attribute); + } + + @Override + public void reportChapterComplete(final User user, final String chapter, final Date date) { + final String attributeName = "Training Complete - " + chapter; + final Attribute attribute = new Attribute(attributeName); + attribute.setStartDate(date); + addAttribute(user, attribute); + } + + private void addAttribute(final User user, final Attribute attribute) { + if (!(user instanceof F1User)) { + throw new IllegalArgumentException("User must be an F1User, but got " + user.getClass().getName()); + } + + try { + final F1User f1User = (F1User) user; + final F1API f1 = mF1Access.getAuthenticatedApi(f1User); + + if (!f1.addAttribute(user.getIdentifier(), attribute)) { + LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute " + + attribute.getAttributeName()); + } + } catch (Exception e) { + LOG.error("addAttribute failed for " + user.getIdentifier() + " with attribute " + + attribute.getAttributeName(), e); + } + } +} diff --git a/src/com/p4square/grow/frontend/ProgressReporter.java b/src/com/p4square/grow/frontend/ProgressReporter.java new file mode 100644 index 0000000..9b57ff4 --- /dev/null +++ b/src/com/p4square/grow/frontend/ProgressReporter.java @@ -0,0 +1,29 @@ +package com.p4square.grow.frontend; + +import org.restlet.security.User; + +import java.util.Date; + +/** + * A ProgressReporter is used to record a User's progress in a Church Management System. + */ +public interface ProgressReporter { + + /** + * Report that the User completed the assessment. + * + * @param user The user who completed the assessment. + * @param level The assessment level. + * @param date The completion date. + * @param results Result information (e.g. json of the results). + */ + void reportAssessmentComplete(User user, String level, Date date, String results); + + /** + * + * @param user The user who completed the chapter. + * @param chapter The chatper completed. + * @param date Teh completion date. + */ + void reportChapterComplete(User user, String chapter, Date date); +} -- cgit v1.2.3 From 6698ffbb300ff8e48b5f3fe59144c699d39ab094 Mon Sep 17 00:00:00 2001 From: Jesse Morgan Date: Tue, 22 Mar 2016 21:42:03 -0700 Subject: Integrating the ProgressReporter Using the new ProgressReporter interface in AssessmentResultsPage and ChapterCompletePage. --- .../f1oauth/FellowshipOneIntegrationDriver.java | 10 +++++++ src/com/p4square/grow/ccb/CCBProgressReporter.java | 32 ++++++++++++++++++++++ .../ChurchCommunityBuilderIntegrationDriver.java | 10 +++++++ .../grow/frontend/AssessmentResultsPage.java | 32 ++++++++-------------- .../grow/frontend/ChapterCompletePage.java | 27 ++++-------------- .../p4square/grow/frontend/IntegrationDriver.java | 9 ++++++ .../p4square/grow/frontend/ProgressReporter.java | 5 ++-- 7 files changed, 80 insertions(+), 45 deletions(-) create mode 100644 src/com/p4square/grow/ccb/CCBProgressReporter.java (limited to 'src/com/p4square/grow/frontend') diff --git a/src/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java b/src/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java index e72df5e..865f5d6 100644 --- a/src/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java +++ b/src/com/p4square/f1oauth/FellowshipOneIntegrationDriver.java @@ -3,6 +3,7 @@ package com.p4square.f1oauth; import com.codahale.metrics.MetricRegistry; import com.p4square.grow.config.Config; import com.p4square.grow.frontend.IntegrationDriver; +import com.p4square.grow.frontend.ProgressReporter; import org.restlet.Context; import org.restlet.security.Verifier; @@ -17,6 +18,8 @@ public class FellowshipOneIntegrationDriver implements IntegrationDriver { private final Config mConfig; private final F1Access mAPI; + private final ProgressReporter mProgressReporter; + public FellowshipOneIntegrationDriver(final Context context) { mContext = context; mConfig = (Config) context.getAttributes().get("com.p4square.grow.config"); @@ -29,6 +32,8 @@ public class FellowshipOneIntegrationDriver implements IntegrationDriver { mConfig.getString("f1ChurchCode", "pfseawa"), F1Access.UserType.WEBLINK); mAPI.setMetricRegistry(mMetricRegistry); + + mProgressReporter = new F1ProgressReporter(mAPI); } /** @@ -42,4 +47,9 @@ public class FellowshipOneIntegrationDriver implements IntegrationDriver { public Verifier newUserAuthenticationVerifier() { return new SecondPartyVerifier(mContext, mAPI); } + + @Override + public ProgressReporter getProgressReporter() { + return mProgressReporter; + } } diff --git a/src/com/p4square/grow/ccb/CCBProgressReporter.java b/src/com/p4square/grow/ccb/CCBProgressReporter.java new file mode 100644 index 0000000..e2304fe --- /dev/null +++ b/src/com/p4square/grow/ccb/CCBProgressReporter.java @@ -0,0 +1,32 @@ +package com.p4square.grow.ccb; + +import com.p4square.ccbapi.CCBAPI; +import com.p4square.grow.frontend.ProgressReporter; +import org.restlet.security.User; + +import java.util.Date; + +/** + * A ProgressReporter which records progress in CCB. + * + * Except not really, because it's not implemented yet. + * This is just a placeholder until ccb-api-client-java has support for updating an individual. + */ +public class CCBProgressReporter implements ProgressReporter { + + private final CCBAPI mAPI; + + public CCBProgressReporter(final CCBAPI api) { + mAPI = api; + } + + @Override + public void reportAssessmentComplete(User user, String level, Date date, String results) { + // TODO + } + + @Override + public void reportChapterComplete(User user, String chapter, Date date) { + // TODO + } +} diff --git a/src/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java b/src/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java index 3aeca2c..48d143c 100644 --- a/src/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java +++ b/src/com/p4square/grow/ccb/ChurchCommunityBuilderIntegrationDriver.java @@ -5,6 +5,7 @@ import com.p4square.ccbapi.CCBAPI; import com.p4square.ccbapi.CCBAPIClient; import com.p4square.grow.config.Config; import com.p4square.grow.frontend.IntegrationDriver; +import com.p4square.grow.frontend.ProgressReporter; import org.restlet.Context; import org.restlet.security.Verifier; @@ -22,6 +23,8 @@ public class ChurchCommunityBuilderIntegrationDriver implements IntegrationDrive private final CCBAPI mAPI; + private final CCBProgressReporter mProgressReporter; + public ChurchCommunityBuilderIntegrationDriver(final Context context) { mContext = context; mConfig = (Config) context.getAttributes().get("com.p4square.grow.config"); @@ -38,6 +41,8 @@ public class ChurchCommunityBuilderIntegrationDriver implements IntegrationDrive mAPI = api; + mProgressReporter = new CCBProgressReporter(mAPI); + } catch (URISyntaxException e) { throw new RuntimeException(e); } @@ -47,4 +52,9 @@ public class ChurchCommunityBuilderIntegrationDriver implements IntegrationDrive public Verifier newUserAuthenticationVerifier() { return new CCBUserVerifier(mAPI); } + + @Override + public ProgressReporter getProgressReporter() { + return mProgressReporter; + } } diff --git a/src/com/p4square/grow/frontend/AssessmentResultsPage.java b/src/com/p4square/grow/frontend/AssessmentResultsPage.java index 9b66794..f1c924b 100644 --- a/src/com/p4square/grow/frontend/AssessmentResultsPage.java +++ b/src/com/p4square/grow/frontend/AssessmentResultsPage.java @@ -7,6 +7,7 @@ package com.p4square.grow.frontend; import java.util.Date; import java.util.Map; +import com.fasterxml.jackson.core.JsonProcessingException; import com.p4square.f1oauth.FellowshipOneIntegrationDriver; import freemarker.template.Template; @@ -30,6 +31,7 @@ import com.p4square.f1oauth.F1User; import com.p4square.grow.config.Config; import com.p4square.grow.provider.JsonEncodedProvider; +import org.restlet.security.User; /** * This page fetches the user's final score and displays the transitional page between @@ -103,30 +105,18 @@ public class AssessmentResultsPage extends FreeMarkerPageResource { } private void publishScoreInF1(Map results) { - if (!(getRequest().getClientInfo().getUser() instanceof F1User)) { - // Only useful if the user is from F1. - return; - } + final ProgressReporter reporter = mGrowFrontend.getThirdPartyIntegrationFactory().getProgressReporter(); - F1User user = (F1User) getRequest().getClientInfo().getUser(); + try { + final User user = getRequest().getClientInfo().getUser(); + final String level = results.get("result").toString(); + final Date completionDate = new Date(); + final String data = JsonEncodedProvider.MAPPER.writeValueAsString(results); - // Update the attribute. - String attributeName = "Assessment Complete - " + results.get("result"); + reporter.reportAssessmentComplete(user, level, completionDate, data); - try { - Attribute attribute = new Attribute(attributeName); - attribute.setStartDate(new Date()); - attribute.setComment(JsonEncodedProvider.MAPPER.writeValueAsString(results)); - - F1API f1 = ((FellowshipOneIntegrationDriver) mGrowFrontend.getThirdPartyIntegrationFactory()) - .getF1Access().getAuthenticatedApi(user); - if (!f1.addAttribute(user.getIdentifier(), attribute)) { - LOG.error("addAttribute failed for " + user.getIdentifier() - + " with attribute " + attributeName); - } - } catch (Exception e) { - LOG.error("addAttribute failed for " + user.getIdentifier() - + " with attribute " + attributeName, e); + } catch (JsonProcessingException e) { + LOG.error("Failed to generate json " + e.getMessage(), e); } } diff --git a/src/com/p4square/grow/frontend/ChapterCompletePage.java b/src/com/p4square/grow/frontend/ChapterCompletePage.java index 2dd1ecf..35abc43 100644 --- a/src/com/p4square/grow/frontend/ChapterCompletePage.java +++ b/src/com/p4square/grow/frontend/ChapterCompletePage.java @@ -32,6 +32,7 @@ import com.p4square.grow.config.Config; import com.p4square.grow.model.TrainingRecord; import com.p4square.grow.provider.Provider; import com.p4square.grow.provider.TrainingRecordProvider; +import org.restlet.security.User; /** * This resource displays the transitional page between chapters. @@ -159,30 +160,12 @@ public class ChapterCompletePage extends FreeMarkerPageResource { } private void assignAttribute() { - if (!(getRequest().getClientInfo().getUser() instanceof F1User)) { - // Only useful if the user is from F1. - return; - } - - F1User user = (F1User) getRequest().getClientInfo().getUser(); + final ProgressReporter reporter = mGrowFrontend.getThirdPartyIntegrationFactory().getProgressReporter(); - // Update the attribute. - String attributeName = "Training Complete - " + mChapter; + final User user = getRequest().getClientInfo().getUser(); + final Date completionDate = new Date(); - try { - Attribute attribute = new Attribute(attributeName); - attribute.setStartDate(new Date()); - - F1API f1 = ((FellowshipOneIntegrationDriver) mGrowFrontend.getThirdPartyIntegrationFactory()) - .getF1Access().getAuthenticatedApi(user); - if (!f1.addAttribute(user.getIdentifier(), attribute)) { - LOG.error("addAttribute failed for " + user.getIdentifier() - + " with attribute " + attributeName); - } - } catch (Exception e) { - LOG.error("addAttribute failed for " + user.getIdentifier() - + " with attribute " + attributeName, e); - } + reporter.reportChapterComplete(user, mChapter, completionDate); } /** diff --git a/src/com/p4square/grow/frontend/IntegrationDriver.java b/src/com/p4square/grow/frontend/IntegrationDriver.java index 3370116..b9c3508 100644 --- a/src/com/p4square/grow/frontend/IntegrationDriver.java +++ b/src/com/p4square/grow/frontend/IntegrationDriver.java @@ -14,4 +14,13 @@ public interface IntegrationDriver { * @return A Verifier. */ Verifier newUserAuthenticationVerifier(); + + /** + * Return a ProgressReporter for this Church Management System. + * + * The ProgressReporter should be thread-safe. + * + * @return The ProgressReporter. + */ + ProgressReporter getProgressReporter(); } diff --git a/src/com/p4square/grow/frontend/ProgressReporter.java b/src/com/p4square/grow/frontend/ProgressReporter.java index 9b57ff4..2f36832 100644 --- a/src/com/p4square/grow/frontend/ProgressReporter.java +++ b/src/com/p4square/grow/frontend/ProgressReporter.java @@ -20,10 +20,11 @@ public interface ProgressReporter { void reportAssessmentComplete(User user, String level, Date date, String results); /** + * Report that the User completed the chapter. * * @param user The user who completed the chapter. - * @param chapter The chatper completed. - * @param date Teh completion date. + * @param chapter The chapter completed. + * @param date The completion date. */ void reportChapterComplete(User user, String chapter, Date date); } -- cgit v1.2.3