Updates 💯

This commit is contained in:
Paul Reilly 2023-05-18 23:15:17 -05:00
parent 1ecff910ec
commit 47fcda6462
30 changed files with 546 additions and 66 deletions

View File

@ -0,0 +1,66 @@
package me.totalfreedom.datura.banning;
import me.totalfreedom.security.ban.BanID;
import java.time.Instant;
import java.time.temporal.ChronoField;
public final class BanUID implements BanID
{
private final char prefix;
private final int numericalTag;
private BanUID(final boolean permanent)
{
if (permanent)
{
prefix = 'P';
} else
{
prefix = 'T';
}
final Instant instant = Instant.now();
String stringBuilder = String.valueOf(instant.get(ChronoField.DAY_OF_YEAR)) + // The first three numbers between 001 -> 365
instant.get(ChronoField.HOUR_OF_DAY) + // next two numbers between 00 -> 23
instant.get(ChronoField.MINUTE_OF_HOUR) + // next two numbers between 00 -> 59
instant.get(ChronoField.MILLI_OF_SECOND); // last three numbers between 000 -> 999
numericalTag = Integer.parseInt(stringBuilder);
}
public static BanUID createTempID()
{
return new BanUID(false);
}
public static BanUID createPermID()
{
return new BanUID(true);
}
@Override
public String getID()
{
return getIDPrefix() + "-" + getNumericalTag();
}
@Override
public char getIDPrefix()
{
return prefix;
}
@Override
public int getNumericalTag()
{
return numericalTag;
}
@Override
public boolean isPermanent()
{
return getIDPrefix() == 'P';
}
}

View File

@ -0,0 +1,82 @@
package me.totalfreedom.datura.banning;
import me.totalfreedom.security.ban.Ban;
import me.totalfreedom.security.ban.BanID;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.util.UUID;
public final class SimpleBan implements Ban
{
private final BanID id;
private final UUID offenderID;
private final String reason;
private final String issuer;
private final Instant creationTime;
private final Instant expiry;
public SimpleBan(
final UUID offenderID,
final String reason,
final String issuer,
final Instant creationTime,
final Instant expiry)
{
if (expiry == null)
{
this.id = BanUID.createPermID();
} else
{
this.id = BanUID.createTempID();
}
this.offenderID = offenderID;
this.reason = reason;
this.issuer = issuer;
this.creationTime = creationTime;
this.expiry = expiry;
}
@Override
public BanID getBanID()
{
return id;
}
@Override
public UUID getOffenderID()
{
return offenderID;
}
@Override
public String getReason()
{
return reason;
}
@Override
public String getBanIssuer()
{
return issuer;
}
@Override
public Instant getCreationTime()
{
return creationTime;
}
@Override
public @Nullable Instant getExpiry()
{
return expiry;
}
@Override
public boolean isExpired()
{
return Instant.now().compareTo(expiry) >= 0;
}
}

View File

@ -0,0 +1,25 @@
package me.totalfreedom.datura.event;
import me.totalfreedom.event.FEvent;
import me.totalfreedom.user.UserData;
public class UserDataUpdateEvent extends FEvent
{
private final UserData data;
public UserDataUpdateEvent(UserData data)
{
this.data = data;
}
public UserData getData()
{
return data;
}
@Override
public Class<? extends FEvent> getEventClass()
{
return UserDataUpdateEvent.class;
}
}

View File

@ -1,7 +1,7 @@
package me.totalfreedom.datura.perms; package me.totalfreedom.datura.perms;
import me.totalfreedom.security.Node; import me.totalfreedom.security.perm.Node;
import me.totalfreedom.security.NodeType; import me.totalfreedom.security.perm.NodeType;
public class DefaultNodes public class DefaultNodes
{ {

View File

@ -1,8 +1,8 @@
package me.totalfreedom.datura.perms; package me.totalfreedom.datura.perms;
import me.totalfreedom.base.CommonsBase; import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.security.Group; import me.totalfreedom.security.perm.Group;
import me.totalfreedom.security.Node; import me.totalfreedom.security.perm.Node;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
import org.bukkit.permissions.Permission; import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionAttachment; import org.bukkit.permissions.PermissionAttachment;

View File

@ -3,7 +3,7 @@ package me.totalfreedom.datura.perms;
import me.totalfreedom.base.CommonsBase; import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.datura.Datura; import me.totalfreedom.datura.Datura;
import me.totalfreedom.datura.user.SimpleUserData; import me.totalfreedom.datura.user.SimpleUserData;
import me.totalfreedom.security.Node; import me.totalfreedom.security.perm.Node;
import me.totalfreedom.user.User; import me.totalfreedom.user.User;
import me.totalfreedom.user.UserData; import me.totalfreedom.user.UserData;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;

View File

@ -1,7 +1,7 @@
package me.totalfreedom.datura.perms; package me.totalfreedom.datura.perms;
import me.totalfreedom.security.Node; import me.totalfreedom.security.perm.Node;
import me.totalfreedom.security.NodeType; import me.totalfreedom.security.perm.NodeType;
import org.bukkit.permissions.Permission; import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault; import org.bukkit.permissions.PermissionDefault;

View File

@ -1,8 +1,8 @@
package me.totalfreedom.datura.perms; package me.totalfreedom.datura.perms;
import me.totalfreedom.security.Node; import me.totalfreedom.security.perm.Node;
import me.totalfreedom.security.NodeBuilder; import me.totalfreedom.security.perm.NodeBuilder;
import me.totalfreedom.security.NodeType; import me.totalfreedom.security.perm.NodeType;
public class PermissionNodeBuilder implements NodeBuilder public class PermissionNodeBuilder implements NodeBuilder
{ {

View File

@ -0,0 +1,89 @@
package me.totalfreedom.datura.sql;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.datura.banning.SimpleBan;
import me.totalfreedom.security.ban.Ban;
import me.totalfreedom.security.ban.BanID;
import me.totalfreedom.sql.SQL;
import me.totalfreedom.utils.FreedomLogger;
import java.sql.SQLException;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class DBBan
{
private final SQL sql;
public DBBan(SQL sql)
{
this.sql = sql;
}
public CompletableFuture<Ban> fromSQL(BanID id)
{
return sql.executeQuery("SELECT * FROM bans WHERE id = ?", id.getID())
.thenApplyAsync(result ->
{
try
{
if (result.next())
{
UUID uuid = UUID.fromString(result.getString("uuid"));
Instant timestamp = Instant.parse(result.getString("timestamp"));
final Instant expiry;
String ex = result.getString("expiry");
if (ex.equals("-1"))
{
expiry = null;
} else
{
expiry = Instant.parse(ex);
}
return new SimpleBan(uuid,
result.getString("reason"),
result.getString("issuer"),
timestamp,
expiry);
}
} catch (SQLException e)
{
FreedomLogger.getLogger("Datura")
.error(e.getMessage());
}
return null;
}, CommonsBase.getInstance().getExecutor().getAsync());
}
public void addBan(Ban ban)
{
sql.executeUpdate("INSERT INTO bans (id, uuid, reason, issuer, timestamp, expiry) VALUES (?, ?, ?, ?, ?, ?)",
ban.getBanID().getID(),
ban.getOffenderID().toString(),
ban.getReason(),
ban.getBanIssuer(),
ban.getCreationTime().toString(),
(ban.getExpiry() != null ? ban.getExpiry().toString() : "-1")
);
}
public boolean hasEntry(UUID uuid) {
return sql.executeQuery("SELECT * FROM bans WHERE uuid = ?", uuid.toString())
.thenApplyAsync(result ->
{
try
{
return result.next();
} catch (SQLException e)
{
FreedomLogger.getLogger("Datura")
.error(e.getMessage());
}
return false;
}, CommonsBase.getInstance().getExecutor().getAsync())
.join();
}
}

View File

@ -1,5 +1,6 @@
package me.totalfreedom.datura.sql; package me.totalfreedom.datura.sql;
import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.sql.SQL; import me.totalfreedom.sql.SQL;
import java.sql.*; import java.sql.*;
@ -39,7 +40,7 @@ public class MySQL implements SQL
throw new CompletionException("Failed to connect to the database: " throw new CompletionException("Failed to connect to the database: "
+ url + "\n", ex); + url + "\n", ex);
} }
}); }, CommonsBase.getInstance().getExecutor().getAsync());
} }
@Override @Override
@ -57,7 +58,7 @@ public class MySQL implements SQL
throw new CompletionException("Failed to prepare statement: " throw new CompletionException("Failed to prepare statement: "
+ query + "\n", ex); + query + "\n", ex);
} }
}); }, CommonsBase.getInstance().getExecutor().getAsync());
} }
@Override @Override
@ -71,7 +72,7 @@ public class MySQL implements SQL
throw new CompletionException("Failed to retrieve a result set from query: " throw new CompletionException("Failed to retrieve a result set from query: "
+ query + "\n", ex); + query + "\n", ex);
} }
}); }, CommonsBase.getInstance().getExecutor().getAsync());
} }
@Override @Override
@ -85,7 +86,7 @@ public class MySQL implements SQL
throw new CompletionException("Failed to execute update: " throw new CompletionException("Failed to execute update: "
+ query + "\n", ex); + query + "\n", ex);
} }
}); }, CommonsBase.getInstance().getExecutor().getAsync());
} }
@Override @Override
@ -99,7 +100,7 @@ public class MySQL implements SQL
throw new CompletionException("Failed to execute statement: " throw new CompletionException("Failed to execute statement: "
+ query + "\n", ex); + query + "\n", ex);
} }
}); }, CommonsBase.getInstance().getExecutor().getAsync());
} }
@Override @Override

View File

@ -1,8 +1,9 @@
package me.totalfreedom.datura.user; package me.totalfreedom.datura.user;
import me.totalfreedom.base.CommonsBase; import me.totalfreedom.base.CommonsBase;
import me.totalfreedom.datura.event.UserDataUpdateEvent;
import me.totalfreedom.datura.perms.FreedomUser; import me.totalfreedom.datura.perms.FreedomUser;
import me.totalfreedom.security.Group; import me.totalfreedom.security.perm.Group;
import me.totalfreedom.sql.SQL; import me.totalfreedom.sql.SQL;
import me.totalfreedom.user.User; import me.totalfreedom.user.User;
import me.totalfreedom.user.UserData; import me.totalfreedom.user.UserData;
@ -21,6 +22,7 @@ public class SimpleUserData implements UserData
private final UUID uuid; private final UUID uuid;
private final String username; private final String username;
private final User user; private final User user;
private final UserDataUpdateEvent event = new UserDataUpdateEvent(this);
private Group group; private Group group;
private long playtime; private long playtime;
private boolean frozen; private boolean frozen;
@ -32,6 +34,8 @@ public class SimpleUserData implements UserData
this.uuid = player.getUniqueId(); this.uuid = player.getUniqueId();
this.username = player.getName(); this.username = player.getName();
this.user = new FreedomUser(player); this.user = new FreedomUser(player);
CommonsBase.getInstance().getEventBus().addEvent(event);
} }
private SimpleUserData( private SimpleUserData(
@ -86,17 +90,16 @@ public class SimpleUserData implements UserData
} }
} catch (SQLException ex) } catch (SQLException ex)
{ {
StringBuilder sb = new StringBuilder(); String sb = "An error occurred while trying to retrieve user data for UUID " +
sb.append("An error occurred while trying to retrieve user data for UUID ") uuid +
.append(uuid) " from the database." +
.append(" from the database.") "\nCaused by: " +
.append("\nCaused by: ") ExceptionUtils.getRootCauseMessage(ex) +
.append(ExceptionUtils.getRootCauseMessage(ex)) "\nStack trace: " +
.append("\nStack trace: ") ExceptionUtils.getStackTrace(ex);
.append(ExceptionUtils.getStackTrace(ex));
FreedomLogger.getLogger("Datura") FreedomLogger.getLogger("Datura")
.error(sb.toString()); .error(sb);
} }
Player player = Bukkit.getPlayer(UUID.fromString(uuid)); Player player = Bukkit.getPlayer(UUID.fromString(uuid));
@ -135,6 +138,7 @@ public class SimpleUserData implements UserData
@Override @Override
public void setGroup(@Nullable Group group) public void setGroup(@Nullable Group group)
{ {
event.ping();
this.group = group; this.group = group;
} }
@ -147,18 +151,21 @@ public class SimpleUserData implements UserData
@Override @Override
public void setPlaytime(long playtime) public void setPlaytime(long playtime)
{ {
event.ping();
this.playtime = playtime; this.playtime = playtime;
} }
@Override @Override
public void addPlaytime(long playtime) public void addPlaytime(long playtime)
{ {
event.ping();
this.playtime += playtime; this.playtime += playtime;
} }
@Override @Override
public void resetPlaytime() public void resetPlaytime()
{ {
event.ping();
this.playtime = 0L; this.playtime = 0L;
} }
@ -171,6 +178,7 @@ public class SimpleUserData implements UserData
@Override @Override
public void setFrozen(boolean frozen) public void setFrozen(boolean frozen)
{ {
event.ping();
this.frozen = true; this.frozen = true;
} }
@ -183,6 +191,7 @@ public class SimpleUserData implements UserData
@Override @Override
public void setInteractionState(boolean canInteract) public void setInteractionState(boolean canInteract)
{ {
event.ping();
this.canInteract = canInteract; this.canInteract = canInteract;
} }
@ -195,6 +204,7 @@ public class SimpleUserData implements UserData
@Override @Override
public void setCaged(boolean caged) public void setCaged(boolean caged)
{ {
event.ping();
this.caged = caged; this.caged = caged;
} }
} }

View File

@ -41,4 +41,9 @@ public class CommonsBase extends JavaPlugin
{ {
return executor; return executor;
} }
public EventBus getEventBus()
{
return eventBus;
}
} }

View File

@ -0,0 +1,35 @@
package me.totalfreedom.data;
import me.totalfreedom.security.ban.Ban;
import me.totalfreedom.security.ban.BanID;
import me.totalfreedom.sql.SQL;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class BanRegistry
{
private final List<Ban> bansList = new ArrayList<>();
public boolean addBan(Ban ban) {
return bansList.add(ban);
}
public boolean removeBan(Ban ban) {
return bansList.remove(ban);
}
@Nullable
public Ban getBan(BanID banID)
{
for (Ban ban : bansList)
{
if (ban.getBanID().matches(banID))
{
return ban;
}
}
return null;
}
}

View File

@ -2,10 +2,25 @@ package me.totalfreedom.data;
import me.totalfreedom.config.Configuration; import me.totalfreedom.config.Configuration;
import java.util.HashSet; import java.util.HashMap;
import java.util.Set; import java.util.Map;
public class ConfigRegistry public class ConfigRegistry
{ {
Set<Configuration> configurationSet = new HashSet<>(); private final Map<String, Configuration> configurationList = new HashMap<>();
public void register(String name, Configuration configuration)
{
configurationList.put(name, configuration);
}
public void unregister(String name)
{
configurationList.remove(name);
}
public Configuration getConfiguration(String name)
{
return configurationList.get(name);
}
} }

View File

@ -1,6 +1,6 @@
package me.totalfreedom.data; package me.totalfreedom.data;
import me.totalfreedom.security.Group; import me.totalfreedom.security.perm.Group;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
import java.util.ArrayList; import java.util.ArrayList;

View File

@ -24,6 +24,16 @@ public class EventBus extends Service
eventSet.add(event); eventSet.add(event);
} }
public <T extends FEvent> T getEvent(Class<T> eventClass)
{
FEvent e = eventSet.stream()
.filter(event -> event.getEventClass().equals(eventClass))
.findFirst()
.orElse(null);
return eventClass.cast(e);
}
public <T extends FEvent> EventSubscription<T> subscribe(Class<T> eventClass, Callback<T> callback) public <T extends FEvent> EventSubscription<T> subscribe(Class<T> eventClass, Callback<T> callback)
{ {
Context<T> eventContext = () -> eventSet.stream() Context<T> eventContext = () -> eventSet.stream()

View File

@ -1,36 +1,15 @@
package me.totalfreedom.event; package me.totalfreedom.event;
import me.totalfreedom.api.Context; public record EventSubscription<T extends FEvent>(T event, Callback<T> callback)
import java.util.function.Supplier;
public final class EventSubscription<T extends FEvent>
{ {
private final T event;
private final Callback<T> callback;
public EventSubscription(T event, Callback<T> callback)
{
this.event = event;
this.callback = callback;
}
public T getEvent()
{
return event;
}
public boolean cancel() public boolean cancel()
{ {
return getEvent().cancel(); return event().cancel();
} }
public boolean isCancelled() public boolean isCancelled()
{ {
return getEvent().isCancelled(); return event().isCancelled();
}
public Callback<T> getCallback() {
return callback;
} }
} }

View File

@ -1,17 +1,29 @@
package me.totalfreedom.event; package me.totalfreedom.event;
import me.totalfreedom.api.Context;
public abstract class FEvent public abstract class FEvent
{ {
private boolean isCancelled; private boolean isCancelled;
private boolean triggered;
protected FEvent() protected FEvent()
{ {
this.isCancelled = false; this.isCancelled = false;
} }
public abstract void call(Callback<FEvent> callback); public void ping()
{
this.triggered = true;
}
public void reset()
{
this.triggered = false;
}
boolean shouldCall()
{
return triggered;
}
public boolean cancel() public boolean cancel()
{ {

View File

@ -1,5 +1,7 @@
package me.totalfreedom.event; package me.totalfreedom.event;
import com.sun.source.tree.ContinueTree;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -20,6 +22,11 @@ class SubscriptionBox<T extends FEvent>
} }
public void tick() { public void tick() {
subscriptions.forEach(s -> s.getCallback().call(s.getEvent())); subscriptions.forEach(s -> {
if (!s.event().shouldCall()) return;
s.callback().call(s.event());
s.event().reset();
});
} }
} }

View File

@ -6,6 +6,7 @@ import org.bukkit.Location;
import org.bukkit.World; import org.bukkit.World;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.Objects; import java.util.Objects;
@ -113,7 +114,7 @@ public class ContextProvider
return new Location(toWorld(split[0]), toDouble(split[1]), toDouble(split[2]), toDouble(split[3])); return new Location(toWorld(split[0]), toDouble(split[1]), toDouble(split[2]), toDouble(split[3]));
} }
private @Nullable Component toComponent(String string) private @NotNull Component toComponent(String string)
{ {
return Component.text(string); return Component.text(string);
} }

View File

@ -0,0 +1,78 @@
package me.totalfreedom.security.ban;
import org.jetbrains.annotations.Nullable;
import java.time.Instant;
import java.util.UUID;
/**
* Represents a physical ban entry. This is used to store information about a ban,
* such as the player who was banned, the reason for why they were banned, the individual who issued the ban,
* when the ban expires, and when the ban was created.
* <br>
* Ban information is stored in the Database with the {@link BanID} as the PRIMARY KEY.
*/
public interface Ban
{
/**
* Gets the ID of this ban. This is an object which represents a string prefixed with either a T or a P,
* and suffixed with a 6-10 digit numerical code. This is used to identify the ban in the database, and for
* easier ban referencing.
*
* @return The ID of this ban.
*/
BanID getBanID();
/**
* Gets the UUID of the player who was banned. This is formatted as a UUID
* which allows us to retrieve the particular player instance, if applicable, and also
* have a true identifier to check against user logins.
*
* @return The UUID of the player who was banned.
*/
UUID getOffenderID();
/**
* Gets the reason that the player was banned for. Typically, the default reason is "You are banned!".
* We've forced implementations to require a reason, as it's important to know why a player was banned.
*
* @return The reason that the player was banned for.
*/
String getReason();
/**
* Gets the username of the individual who issued the ban. This is not a reliable way to store data, but
* in our case, we should not need to interact with the ban issuer from the code. This is simply for
* reference purposes.
*
* @return The username of the individual who issued the ban.
*/
String getBanIssuer();
/**
* Gets the {@link Instant} which this ban was created.
*
* @return The ban's creation time.
*/
Instant getCreationTime();
/**
* Gets the {@link Instant} which this ban is due to expire, if applicable.
* This method is annotated as {@link Nullable}, as permanent bans do not have an expiry date.
*
* @return The ban's expiry time, or null if the ban is permanent.
*/
@Nullable
Instant getExpiry();
/**
* Checks if the ban has expired. This will return false if:
* <ul>
* <li>The {@link Instant} returned by {@link #getExpiry()} is null.</li>
* <li>The {@link Instant} returned by {@link #getExpiry()} is after the current time.</li>
* </ul>
*
* @return True if the ban has expired, false otherwise.
*/
boolean isExpired();
}

View File

@ -0,0 +1,52 @@
package me.totalfreedom.security.ban;
/**
* Represents an ID for a ban. These are formatted either as:
* <p>
* P-00129381
* <br>
* T-00128381
* <br>
* </p>
* Where P marks a ban as permanent, and T marks a ban as temporary.
*/
public interface BanID
{
/**
* This method returns the full Ban ID.
*
* @return The actual ID.
*/
String getID();
/**
* This method returns the ban type denominator character for the Ban ID.
* This would either be T or P, where T = temporary and P = permanent.
*
* @return The ban type denominator character for the Ban ID.
*/
char getIDPrefix();
/**
* Gets the numerical tag of this ban ID.
* This would be the numerical section of the full Ban ID.
*
* @return The numerical tag of this ban ID.
*/
int getNumericalTag();
/**
* Checks the prefix of the Ban ID to see whether if it is permanent.
*
* @return true if the Ban ID is prefixed with a P, false otherwise.
*/
boolean isPermanent();
default boolean matches(BanID other) {
if (other == null) {
return false;
}
return (getIDPrefix() == other.getIDPrefix())
&& (getNumericalTag() == other.getNumericalTag());
}
}

View File

@ -1,4 +1,4 @@
package me.totalfreedom.security; package me.totalfreedom.security.perm;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;

View File

@ -1,4 +1,4 @@
package me.totalfreedom.security; package me.totalfreedom.security.perm;
import org.bukkit.permissions.Permission; import org.bukkit.permissions.Permission;

View File

@ -1,4 +1,4 @@
package me.totalfreedom.security; package me.totalfreedom.security.perm;
public interface NodeBuilder public interface NodeBuilder
{ {

View File

@ -1,4 +1,4 @@
package me.totalfreedom.security; package me.totalfreedom.security.perm;
public enum NodeType public enum NodeType
{ {

View File

@ -1,6 +1,5 @@
package me.totalfreedom.security; package me.totalfreedom.security.perm;
import org.bukkit.entity.Player;
import org.bukkit.permissions.Permissible; import org.bukkit.permissions.Permissible;
import java.util.Set; import java.util.Set;

View File

@ -1,6 +1,6 @@
package me.totalfreedom.user; package me.totalfreedom.user;
import me.totalfreedom.security.PermissionHolder; import me.totalfreedom.security.perm.PermissionHolder;
import net.kyori.adventure.text.Component; import net.kyori.adventure.text.Component;
public interface User extends PermissionHolder public interface User extends PermissionHolder

View File

@ -1,6 +1,6 @@
package me.totalfreedom.user; package me.totalfreedom.user;
import me.totalfreedom.security.Group; import me.totalfreedom.security.perm.Group;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;

View File

@ -0,0 +1,14 @@
package me.totalfreedom.utils;
public class MICheck
{
public double maintainabilityIndex(double fileLength, double vocabulary, double cyclomaticComplexity, double linesOfCode) {
double halsteadVolume = fileLength * (Math.log(vocabulary) / Math.log(2));
double maintainabilityIndexUnbounded = 171 - 5.2 * Math.log(halsteadVolume) - 0.23 * cyclomaticComplexity - 16.2 * Math.log(linesOfCode);
return Math.max(0, maintainabilityIndexUnbounded * 100 / 171);
}
public double complexity(double decisionPoints) {
return decisionPoints + 1;
}
}