Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 20 additions & 26 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<groupId>io.github.isa-group</groupId>
<artifactId>Pricing4Java</artifactId>
<version>5.5.1</version>
<version>5.6.0-SNAPSHOT</version>

<name>${project.groupId}:${project.artifactId}</name>
<description>A pricing driven feature toggling library for java</description>
Expand Down Expand Up @@ -49,13 +49,14 @@
</distributionManagement>

<properties>
<jackson.version>2.14.2</jackson.version>
<aspectj.version>1.9.7</aspectj.version>
<spring.version>6.1.5</spring.version>
<spring.boot.version>3.2.0</spring.boot.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jjwt.version>0.12.6</jjwt.version>
<aspectj.version>1.9.7</aspectj.version>
<spring.version>6.1.5</spring.version>
<spring.boot.version>3.2.0</spring.boot.version>
<snakeyaml.version>2.3</snakeyaml.version>
</properties>


Expand Down Expand Up @@ -143,42 +144,35 @@
<!-- JSON WEB TOKEN -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20250107</version>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>

<!-- YAML PARSER -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.3</version>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>test</scope>
</dependency>

<!-- API, java.xml.bind module -->
<dependency>
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
<version>2.3.2</version>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>test</scope>
</dependency>

<!-- Runtime, com.sun.xml.bind module -->
<!-- YAML PARSER -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.2</version>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>
</dependencies>

<build>
<finalName>Pricing4Java</finalName>
<plugins>


<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
Expand Down
72 changes: 49 additions & 23 deletions src/main/java/io/github/isagroup/PricingContext.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,44 @@ public abstract class PricingContext {
private static final Logger logger = LoggerFactory.getLogger(PricingContext.class);

/**
* Returns path of the pricing configuration YAML file.
* This file should be located in the resources folder, and the path should be
* relative to it.
* Returns the path to the Pricing2Yaml configuration file. The {@link String}
* path given to the implementation of this method should point to a
* Pricing2Yaml file under {@code resources} folder.
*
* @return Configuration file path
* @return a path as {@link String} to a Pricing2Yaml configuration file
* relative to the {@code resources} folder
*/
public abstract String getConfigFilePath();

/**
* Returns the secret used to encode the pricing JWT.
* * @return JWT secret String
* Returns the secret used to sign the pricing JWT. The secret key needs to be
* encoded in {@code base64}. Internally JWT library will choose the best
* algorithm to sign the JWT ({@code HS256}, {@code HS384} or {@code HS512}), if
* the secret key bit length does not conform to the minimun stated by these
* algorithms will throw a {@link io.jsonwebtoken.security.WeakKeyException}
*
* @return a pricing secret encoded in {@code base64}
* @see <a href=
* "https://github.com/jwtk/jjwt#signature-algorithms-keys">Signature
* Algorithm Keys</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7518#section-3.2">HMAC
* with SHA-2 Functions</a>
*/
public abstract String getJwtSecret();

/**
* Returns the secret used to encode the authorization JWT.
* * @return JWT secret String
* Returns the secret used to sign the authorization JWT. The secret key needs
* to be encoded in {@code base64}. Internally JWT library will choose the best
* algorithm to sign the JWT ({@code HS256}, {@code HS384} or {@code HS512}), if
* the secret key bit length does not conform to the minimun stated by these
* algorithms will throw a {@link io.jsonwebtoken.security.WeakKeyException}
*
* @return a pricing secret encoded in {@code base64}
* @see <a href=
* "https://github.com/jwtk/jjwt#signature-algorithms-keys">Signature
* Algorithm Keys</a>
* @see <a href="https://datatracker.ietf.org/doc/html/rfc7518#section-3.2">HMAC
* with SHA-2 Functions</a>
*/
public String getAuthJwtSecret() {
return this.getJwtSecret();
Expand All @@ -66,7 +87,8 @@ public int getJwtExpiration() {
* for them.
*
* @return A {@link Boolean} indicating the condition to include, or not,
* the pricing evaluation context in the JWT.
* the pricing evaluation context in the JWT. Set to {@code true} by
* default
*
* @see PricingEvaluatorUtil#generateUserToken
*
Expand All @@ -77,9 +99,8 @@ public Boolean userAffectedByPricing() {

/**
* This method should return the user context that will be used to evaluate the
* pricing plan.
* It should be considered which users has accessed the service and what
* information is available.
* pricing plan. It should be considered which users has accessed the service
* and what information is available.
*
* @return Map with the user context
*/
Expand All @@ -90,30 +111,35 @@ public Boolean userAffectedByPricing() {
* With this information, the library will be able to build the {@link Plan}
* object of the user from the configuration.
*
* @return String with the current user's plan name
* @return a {@link String} with the current user's plan name
*/
public abstract String getUserPlan();

/**
* This method should return a list with the name of the add-ons contracted by
* the current user. If the pricing don't include add-ons, then just return an empty array.
* With this information, the library will be able to build the subscription of
* the user from the configuration.
* the current user. If the pricing does not include add-ons, then just return
* an empty {@link List}. With this information, the library will be able to
* build the subscription of the user from the configuration.
*
* @return {@code List<String>} with the current user's contracted add-ons. Add-on names
* should be the same as in the pricing configuration file.
* @return a list with the current user's contracted add-ons.
* Add-on names should be the same as in the pricing configuration file.
*
*/
public abstract List<String> getUserAddOns();

/**
* Returns a list with the full subscription contracted by the current user
* (including plans and add-ons).
* <p>
* There are two keys inside this {@link Map}:
* <ul>
* <li>Key {@code plans} contains the plan name of the user
* <li>Key {@code addOns} contains a list with the add-ons contracted by the
* user
* </ul>
*
* Key "plan" contains the plan name of the user.
* Key "addOns" contains a list with the add-ons contracted by the user.
*
* @return {@code Map<String, Object>} with the current user's contracted subscription.
* @return {@code Map<String, Object>} with the current user's contracted
* subscription.
*/
public final Map<String, Object> getUserSubscription() {
Map<String, Object> userSubscription = new HashMap<>();
Expand Down Expand Up @@ -164,7 +190,7 @@ public final Map<String, Object> getPlanContext() {
* This method returns the {@link PricingManager} object that is being used to
* evaluate the pricing plan.
*
* @return PricingManager object
* @return {@link PricingManager} object
*/
public final PricingManager getPricingManager() {
try {
Expand Down
58 changes: 24 additions & 34 deletions src/main/java/io/github/isagroup/PricingEvaluatorUtil.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package io.github.isagroup;

import java.util.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.logging.Logger;

import io.github.isagroup.models.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.stereotype.Component;

import io.github.isagroup.exceptions.PricingPlanEvaluationException;
import io.github.isagroup.models.Feature;
import io.github.isagroup.models.FeatureStatus;
import io.github.isagroup.models.PlanContextManager;
import io.github.isagroup.models.PricingManager;
import io.github.isagroup.services.jwt.PricingJwtUtils;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

/**
* Utility class that provides methods to generate and manage JWT that contains
Expand Down Expand Up @@ -58,23 +61,19 @@ public String generateUserToken() {
planContextManager.setUserContext(pricingContext.getUserContext());
claims.put("userContext", planContextManager.getUserContext());
} catch (Exception e) {
throw new PricingPlanEvaluationException("Error while retrieving user context! Please check your PricingContext.getUserContext() method");
throw new PricingPlanEvaluationException(
"Error while retrieving user context! Please check your PricingContext.getUserContext() method");
}

if (!pricingContext.userAffectedByPricing()) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + pricingContext.getJwtExpiration()))
.signWith(SignatureAlgorithm.HS512, pricingContext.getJwtSecret())
.compact();
return jwtUtils.createJwtToken(claims, subject);
}

try {
planContextManager.setPlanContext(pricingContext.getPlanContext());
} catch (NullPointerException e) {
throw new PricingPlanEvaluationException("Error while retrieving plan context! Please check your configuration file or add a plan with the given name");
throw new PricingPlanEvaluationException(
"Error while retrieving plan context! Please check your configuration file or add a plan with the given name");
}

PricingManager pricingManager = pricingContext.getPricingManager();
Expand All @@ -86,18 +85,11 @@ public String generateUserToken() {
claims.put("features", featureStatuses);
claims.put("planContext", planContextManager.getPlanContext());

return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + pricingContext.getJwtExpiration()))
.signWith(SignatureAlgorithm.HS512, pricingContext.getJwtSecret())
.compact();
return jwtUtils.createJwtToken(claims, subject);
}


private Map<String, FeatureStatus> computeFeatureStatuses(PlanContextManager planContextManager,
Map<String, Feature> features) {
Map<String, Feature> features) {

Map<String, FeatureStatus> featureStatuses = new HashMap<>();

Expand All @@ -109,10 +101,11 @@ private Map<String, FeatureStatus> computeFeatureStatuses(PlanContextManager pla
String expression = features.get(featureName).getExpression();
try {
Boolean eval = FeatureStatus.computeFeatureEvaluation(expression, planContextManager)
.orElseThrow(() -> new PricingPlanEvaluationException("Evaluation was null"));
.orElseThrow(() -> new PricingPlanEvaluationException("Evaluation was null"));
featureStatus.setEval(eval);
} catch (SpelEvaluationException e) {
throw new PricingPlanEvaluationException("Error while evaluating the expression of the feature " + featureName + "! Please check the expression");
throw new PricingPlanEvaluationException("Error while evaluating the expression of the feature "
+ featureName + "! Please check the expression");
}

Optional<String> userContextKey = FeatureStatus.computeUserContextVariable(expression);
Expand All @@ -124,9 +117,12 @@ private Map<String, FeatureStatus> computeFeatureStatuses(PlanContextManager pla
featureStatus.setUsed(planContextManager.getUserContext().get(userContextKey.get()));
if (feature.getExpression().contains("usageLimits")) {
String usageLimitName = feature.getExpression().split("usageLimits")[1].split("[',\"]")[2];
featureStatus.setLimit(((Map<String, Object>) planContextManager.getPlanContext().get("usageLimits")).get(usageLimitName));
featureStatus
.setLimit(((Map<String, Object>) planContextManager.getPlanContext().get("usageLimits"))
.get(usageLimitName));
} else {
featureStatus.setLimit(((Map<String, Object>) planContextManager.getPlanContext().get("features")).get(featureName));
featureStatus.setLimit(((Map<String, Object>) planContextManager.getPlanContext().get("features"))
.get(featureName));
}

}
Expand All @@ -148,7 +144,7 @@ private Map<String, FeatureStatus> computeFeatureStatuses(PlanContextManager pla
* @param expression the expression of the feature that will replace its
* evaluation
* @return Modified version of the provided JWT that contains the new expression
* in the "eval" attribute of the feature.
* in the "eval" attribute of the feature.
*/
public String addExpressionToToken(String token, String featureId, String expression) {

Expand All @@ -173,13 +169,7 @@ private String buildJwtToken(Map<String, Map<String, Object>> features, String s
claims.put("userContext", pricingContext.getUserContext());
claims.put("planContext", pricingContext.getPlanContext());

return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + pricingContext.getJwtExpiration()))
.signWith(SignatureAlgorithm.HS512, pricingContext.getJwtSecret())
.compact();
return jwtUtils.createJwtToken(claims, subject);
}

}
Loading