diff --git a/pom.xml b/pom.xml index fd63243..8c57e59 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ eBUS core library - This library handles the communication with heating engineering via the BUS specification. This protocol is used by many heating manufacturers in Europe. de.cs-dev.ebus ebus-core - 1.1.14 + 1.2.0 https://github.com/csowada/ebus bundle @@ -307,21 +307,14 @@ ch.qos.logback logback-classic - 1.5.16 + 1.5.20 test org.slf4j slf4j-api - 1.7.32 - provided - - - - org.apache.commons - commons-lang3 - 3.12.0 + 1.7.36 provided @@ -336,7 +329,7 @@ com.fazecast jSerialComm - 2.11.0 + 2.11.2 provided true @@ -344,7 +337,7 @@ com.google.code.gson gson - 2.9.1 + 2.10.1 provided diff --git a/src/main/java/de/csdev/ebus/cfg/std/EBusConfigurationReader.java b/src/main/java/de/csdev/ebus/cfg/std/EBusConfigurationReader.java index e397262..92cacc9 100644 --- a/src/main/java/de/csdev/ebus/cfg/std/EBusConfigurationReader.java +++ b/src/main/java/de/csdev/ebus/cfg/std/EBusConfigurationReader.java @@ -27,7 +27,6 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; -import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; @@ -56,25 +55,47 @@ import de.csdev.ebus.command.datatypes.std.EBusTypeByte; import de.csdev.ebus.core.EBusConsts; import de.csdev.ebus.utils.EBusUtils; +import de.csdev.ebus.utils.StringUtil; /** * @author Christian Sowada - Initial contribution * */ +/** + * A configuration reader for eBus configurations that loads and parses + * configuration files + * in JSON format. This reader handles templates, command methods, and value + * configurations + * while maintaining type safety and null safety. + * + * @author Christian Sowada - Initial contribution + */ public class EBusConfigurationReader implements IEBusConfigurationReader { + private static final int INITIAL_MAP_CAPACITY = 32; + private static final String ERROR_REGISTRY_INIT = "Unable to create a new eBus type registry!"; + private final Logger logger = LoggerFactory.getLogger(EBusConfigurationReader.class); private @NonNull EBusTypeRegistry registry; - private @NonNull Map<@NonNull String, @Nullable Collection<@NonNull EBusCommandValue>> templateValueRegistry = new HashMap<>(); - private @NonNull Map<@NonNull String, @Nullable Collection<@NonNull EBusCommandValue>> templateBlockRegistry = new HashMap<>(); + // Initialize maps with a reasonable initial capacity to avoid resizing + private @NonNull Map<@NonNull String, @Nullable Collection<@NonNull EBusCommandValue>> templateValueRegistry = new HashMap<>( + INITIAL_MAP_CAPACITY); + private @NonNull Map<@NonNull String, @Nullable Collection<@NonNull EBusCommandValue>> templateBlockRegistry = new HashMap<>( + INITIAL_MAP_CAPACITY); + /** + * Creates a new EBusConfigurationReader instance. + * Initializes the type registry and template registries. + * + * @throws IllegalStateException if the type registry cannot be created + */ public EBusConfigurationReader() { try { this.registry = new EBusTypeRegistry(); } catch (EBusTypeException e) { - throw new IllegalStateException("Unable to create a new eBus type registry!"); + throw new IllegalStateException(ERROR_REGISTRY_INIT, e); } } @@ -137,20 +158,28 @@ public EBusConfigurationReader() { } } + /* + * (non-Javadoc) + * + * @see + * de.csdev.ebus.cfg.IEBusConfigurationReader#loadConfigurationCollection(de. + * csdev. + * ebus.cfg.std.dto.EBusCollectionDTO) + */ public @NonNull IEBusCommandCollection loadConfigurationCollection(@NonNull EBusCollectionDTO collection) throws EBusConfigurationReaderException { Objects.requireNonNull(collection, "collection"); - if (StringUtils.isEmpty(collection.getId())) { + if (StringUtil.isEmpty(collection.getId())) { throw new EBusConfigurationReaderException("The property 'id' is missing for the configuration!"); } - if (StringUtils.isEmpty(collection.getLabel())) { + if (StringUtil.isEmpty(collection.getLabel())) { throw new EBusConfigurationReaderException("The property 'label' is missing for the configuration!"); } - if (StringUtils.isEmpty(collection.getDescription())) { + if (StringUtil.isEmpty(collection.getDescription())) { throw new EBusConfigurationReaderException("The property 'description' is missing for the configuration!"); } @@ -158,8 +187,14 @@ public EBusConfigurationReader() { throw new EBusConfigurationReaderException("The property 'properties' is missing for the configuration!"); } - EBusCommandCollection commandCollection = new EBusCommandCollection(collection.getId(), collection.getLabel(), - collection.getDescription(), collection.getProperties()); + String id = Objects.requireNonNull(collection.getId(), "Collection ID must not be null"); + String label = Objects.requireNonNull(collection.getLabel(), "Collection label must not be null"); + String description = Objects.requireNonNull(collection.getDescription(), + "Collection description must not be null"); + Map properties = Objects.requireNonNull(collection.getProperties(), + "Collection properties must not be null"); + + EBusCommandCollection commandCollection = new EBusCommandCollection(id, label, description, properties); // add md5 hash commandCollection.setIdentification(collection.getIdentification()); @@ -179,6 +214,15 @@ public EBusConfigurationReader() { return commandCollection; } + /** + * Parses the template list configuration from the given collection. + * This method processes all template blocks defined in the collection + * and adds them to the template registries for later use. + * + * @param collection The eBus collection containing template definitions + * @throws EBusConfigurationReaderException if there's an error parsing + * templates + */ protected void parseTemplateListConfiguration(@NonNull EBusCollectionDTO collection) throws EBusConfigurationReaderException { @@ -193,10 +237,27 @@ protected void parseTemplateListConfiguration(@NonNull EBusCollectionDTO collect } } - protected void parseTemplateBlockConfiguration(@Nullable List templateValues, @NonNull EBusCollectionDTO collection, @NonNull EBusCommandTemplatesDTO templates) + /** + * Parses template block configuration and adds templates to the registry. + * This method processes template values and creates corresponding command + * values + * that can be reused throughout the configuration. + * + * @param templateValues The list of template values to parse, may be null + * @param collection The parent collection containing these templates + * @param templates The template configuration containing name and other + * properties + * @throws EBusConfigurationReaderException if there's an error parsing the + * templates + */ + protected void parseTemplateBlockConfiguration(@Nullable List templateValues, + @NonNull EBusCollectionDTO collection, @NonNull EBusCommandTemplatesDTO templates) throws EBusConfigurationReaderException { - if (templateValues == null) { + Objects.requireNonNull(collection, "collection cannot be null"); + Objects.requireNonNull(templates, "templates cannot be null"); + + if (templateValues == null || templateValues.isEmpty()) { return; } @@ -204,32 +265,39 @@ protected void parseTemplateBlockConfiguration(@Nullable List temp for (EBusValueDTO value : templateValues) { if (value != null) { - Collection<@NonNull EBusCommandValue> pv = parseValueConfiguration(value, null, null, null); + Collection<@NonNull EBusCommandValue> parsedValues = parseValueConfiguration(value, null, null, null); + + if (!parsedValues.isEmpty()) { + blockList.addAll(parsedValues); - if (!pv.isEmpty()) { - blockList.addAll(pv); + // Build global id with StringBuilder for better performance + String id = new StringBuilder(50) + .append(collection.getId()) + .append('.') + .append(templates.getName()) + .append('.') + .append(value.getName()) + .toString(); - // global id - String id = collection.getId() + "." + templates.getName() + "." + value.getName(); logger.trace("Add template with global id {} to registry ...", id); - templateValueRegistry.put(id, pv); + templateValueRegistry.put(id, parsedValues); } } } if (!blockList.isEmpty()) { String id = collection.getId() + "." + templates.getName(); - - // global id logger.trace("Add template block with global id {} to registry ...", id); templateBlockRegistry.put(id, blockList); } } /** - * @param commandCollection - * @param commandElement - * @return + * Parses the command configuration from the given collection. + * + * @param commandCollection The eBus command collection + * @param commandElement The eBus command element to parse + * @return The parsed eBus command * @throws EBusConfigurationReaderException */ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection commandCollection, @@ -263,8 +331,9 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection // read in template block for (EBusValueDTO template : checkedList(commandElement.getTemplate())) { for (EBusCommandValue templateCfg : parseValueConfiguration(template, null, null, null)) { - if (StringUtils.isEmpty(templateCfg.getName())) { - templateMap.put(templateCfg.getName(), templateCfg); + String name = templateCfg.getName(); + if (StringUtil.isEmpty(name)) { + templateMap.put(name, templateCfg); } templateList.add(templateCfg); @@ -307,8 +376,9 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection EBusCommandMethod commandMethod = new EBusCommandMethod(cfg, method); // overwrite with local command - if (StringUtils.isNotEmpty(commandMethodElement.getCommand())) { - commandMethod.setCommand(EBusUtils.toByteArray(commandMethodElement.getCommand())); + String methodCommand = commandMethodElement.getCommand(); + if (StringUtil.isNotEmpty(methodCommand)) { + commandMethod.setCommand(EBusUtils.toByteArray(methodCommand)); } else { commandMethod.setCommand(command); } @@ -317,19 +387,22 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection commandMethod.setSourceAddress(source); for (EBusValueDTO template : checkedList(commandMethodElement.getMaster())) { - for (EBusCommandValue ev : parseValueConfiguration(template, templateMap, templateList, commandMethod)) { - commandMethod.addMasterValue(ev); - } + for (EBusCommandValue ev : parseValueConfiguration(template, templateMap, templateList, + commandMethod)) { + commandMethod.addMasterValue(ev); + } } for (EBusValueDTO template : checkedList(commandMethodElement.getSlave())) { - for (EBusCommandValue ev : parseValueConfiguration(template, templateMap, templateList, commandMethod)) { - commandMethod.addSlaveValue(ev); - } + for (EBusCommandValue ev : parseValueConfiguration(template, templateMap, templateList, + commandMethod)) { + commandMethod.addSlaveValue(ev); + } } // default type is always master-slave if not explicit set or a broadcast - if (StringUtils.equals(commandMethodElement.getType(), "master-master")) { + String methodType = commandMethodElement.getType(); + if ("master-master".equals(methodType)) { commandMethod.setType(IEBusCommandMethod.Type.MASTER_MASTER); } else if (method == Method.BROADCAST) { @@ -353,23 +426,31 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection } /** - * Helper function to work with a secure non null list - * @param master - * @return + * Creates a new list containing only non-null elements from the input list. + * This helper function ensures type safety and null safety when working with + * lists + * of EBusValueDTO objects. + * + * @param source The source list that may contain null elements + * @return A new list containing only non-null elements, never null itself */ - protected @NonNull List<@NonNull EBusValueDTO> checkedList(List master) { - List<@NonNull EBusValueDTO> templates = new ArrayList<>(); - if (master != null) { - for (EBusValueDTO template : master) { - if (template != null) { - templates.add(template); - } + protected @NonNull List<@NonNull EBusValueDTO> checkedList(@Nullable List source) { + if (source == null || source.isEmpty()) { + return new ArrayList<>(); + } + + // Pre-size the list to avoid resizing + List<@NonNull EBusValueDTO> templates = new ArrayList<>(source.size()); + for (EBusValueDTO template : source) { + if (template != null) { + templates.add(template); } } return templates; } /** + * Parses the value configuration from the given DTO. * @param valueDto * @param templateMap * @param commandMethod @@ -393,7 +474,7 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection collectionId = commandMethod.getParent().getParentCollection().getId(); } - if (StringUtils.isEmpty(typeStr)) { + if (StringUtil.isEmpty(typeStr)) { throw new EBusConfigurationReaderException("Property 'type' is missing for command ! %s", commandMethod != null ? commandMethod.getParent() : ""); } @@ -402,7 +483,8 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection Collection<@NonNull EBusCommandValue> templateCollection = null; - if (StringUtils.isNotEmpty(valueDto.getName())) { + String valueName = valueDto.getName(); + if (StringUtil.isNotEmpty(valueName)) { logger.warn("Property 'name' is not allowed for type 'template-block', ignore property !"); } @@ -410,12 +492,10 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection String id = (String) valueDto.getProperty("id"); String globalId = collectionId + "." + id; - if (StringUtils.isNotEmpty(id)) { - + if (StringUtil.isNotEmpty(id)) { templateCollection = templateBlockRegistry.get(id); if (templateCollection == null) { - // try to convert the local id to a global id logger.trace("Unable to find a template with id {}, second try with {} ...", id, globalId); @@ -456,7 +536,7 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection String globalId = collectionId != null ? collectionId + "." + id : null; Collection<@NonNull EBusCommandValue> templateCollection = null; - if (StringUtils.isEmpty(id)) { + if (StringUtil.isEmpty(id)) { throw new EBusConfigurationReaderException("No additional information for type 'template' defined!"); } @@ -485,7 +565,8 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection overwritePropertiesFromTemplate(clone, valueDto); // allow owerwrite for single names - clone.setName(StringUtils.defaultIfEmpty(valueDto.getName(), clone.getName())); + clone.setName( + StringUtil.defaultIfEmpty(valueDto.getName(), Objects.requireNonNull(clone.getName()))); result.add(clone); } @@ -570,13 +651,12 @@ protected EBusCommand parseTelegramConfiguration(@NonNull IEBusCommandCollection } private void overwritePropertiesFromTemplate(@NonNull EBusCommandValue clone, @NonNull EBusValueDTO template) { - String templateLabel = template.getLabel(); String cloneLabel = clone.getLabel(); // allow placeholders in template-block mode - if (StringUtils.isNotEmpty(templateLabel)) { - if (StringUtils.isNotEmpty(cloneLabel) && cloneLabel.contains("%s")) { + if (StringUtil.isNotEmpty(templateLabel)) { + if (cloneLabel != null && cloneLabel.contains("%s")) { clone.setLabel(String.format(cloneLabel, templateLabel)); } else { clone.setLabel(templateLabel); @@ -597,32 +677,62 @@ public void setEBusTypes(@NonNull EBusTypeRegistry ebusTypes) { registry = ebusTypes; } + /** + * Loads a bundle of configuration collections from a JSON index file. + * The index file should contain a "files" array with URLs to individual + * configuration files. + * + * @param url The URL to the index file + * @return A list of loaded command collections + * @throws EBusConfigurationReaderException if there's an error parsing + * configurations + * @throws IOException if there's an error reading the + * files + * @throws NullPointerException if url is null + */ @Override public @NonNull List<@NonNull IEBusCommandCollection> loadConfigurationCollectionBundle(@NonNull URL url) - throws EBusConfigurationReaderException, IOException { + throws EBusConfigurationReaderException, IOException { - Objects.requireNonNull(url, "url"); + Objects.requireNonNull(url, "url cannot be null"); List<@NonNull IEBusCommandCollection> result = new ArrayList<>(); + InputStreamReader reader = null; - Gson gson = new Gson(); - Type type = new TypeToken>() { - }.getType(); + try { + Gson gson = new Gson(); + Type type = new TypeToken>() { + }.getType(); - Map mapping = gson.fromJson(new InputStreamReader(url.openStream()), type); + reader = new InputStreamReader(url.openStream()); + Map mapping = gson.fromJson(reader, type); - if (mapping.containsKey("files")) { + if (mapping != null && mapping.containsKey("files")) { + @SuppressWarnings("unchecked") + List> files = (List>) mapping.get("files"); - @SuppressWarnings("unchecked") - List> files = (List>) mapping.get("files"); + if (files != null && !files.isEmpty()) { + // Pre-size the result list + result = new ArrayList<>(files.size()); - if (files != null && !files.isEmpty()) { - for (Map file : files) { - URL fileUrl = new URL(url, file.get("url")); + for (Map file : files) { + String fileUrlStr = file.get("url"); + if (fileUrlStr != null) { + URL fileUrl = new URL(url, fileUrlStr); + logger.debug("Loading configuration from url {} ...", fileUrl); - logger.debug("Load configuration from url {} ...", fileUrl); - IEBusCommandCollection collection = loadConfigurationCollection(fileUrl); - result.add(collection); + IEBusCommandCollection collection = loadConfigurationCollection(fileUrl); + result.add(collection); + } + } + } + } + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + logger.warn("Failed to close reader", e); } } } diff --git a/src/main/java/de/csdev/ebus/cfg/std/EBusValueJsonDeserializer.java b/src/main/java/de/csdev/ebus/cfg/std/EBusValueJsonDeserializer.java index 010a470..af8d95b 100644 --- a/src/main/java/de/csdev/ebus/cfg/std/EBusValueJsonDeserializer.java +++ b/src/main/java/de/csdev/ebus/cfg/std/EBusValueJsonDeserializer.java @@ -14,8 +14,7 @@ import java.util.List; import java.util.Map.Entry; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.eclipse.jdt.annotation.Nullable; import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; @@ -37,9 +36,6 @@ */ public class EBusValueJsonDeserializer implements JsonDeserializer> { - @SuppressWarnings("unused") - private final Logger logger = LoggerFactory.getLogger(EBusValueJsonDeserializer.class); - @Override public List deserialize(JsonElement jElement, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { @@ -47,17 +43,7 @@ public List deserialize(JsonElement jElement, Type typeOfT, JsonDe JsonArray asJsonArray = jElement.getAsJsonArray(); ArrayList result = new ArrayList<>(); - ArrayList fields = new ArrayList<>(); - for (Field field : EBusValueDTO.class.getDeclaredFields()) { - - SerializedName annotation = field.getAnnotation(SerializedName.class); - if (annotation != null) { - fields.add(annotation.value()); - - } else { - fields.add(field.getName()); - } - } + final List fields = getFields(); for (JsonElement jsonElement : asJsonArray) { JsonObject jObject = jsonElement.getAsJsonObject(); @@ -68,16 +54,7 @@ public List deserialize(JsonElement jElement, Type typeOfT, JsonDe if (entry.getValue().isJsonPrimitive()) { JsonPrimitive primitive = (JsonPrimitive) entry.getValue(); - - if (primitive.isNumber()) { - valueDTO.setProperty(entry.getKey(), primitive.getAsBigDecimal()); - - } else if (primitive.isBoolean()) { - valueDTO.setProperty(entry.getKey(), primitive.getAsBoolean()); - - } else if (primitive.isString()) { - valueDTO.setProperty(entry.getKey(), primitive.getAsString()); - } + setPrimitiveProperty(valueDTO, entry, primitive); } else { valueDTO.setProperty(entry.getKey(), entry.getValue().getAsString()); @@ -93,4 +70,43 @@ public List deserialize(JsonElement jElement, Type typeOfT, JsonDe return result; } + /** + * Set a property on the given valueDTO based on the type of the primitive + * + * @param valueDTO + * @param entry + * @param primitive + */ + private void setPrimitiveProperty(EBusValueDTO valueDTO, Entry entry, JsonPrimitive primitive) { + if (primitive.isNumber()) { + valueDTO.setProperty(entry.getKey(), primitive.getAsBigDecimal()); + + } else if (primitive.isBoolean()) { + valueDTO.setProperty(entry.getKey(), primitive.getAsBoolean()); + + } else if (primitive.isString()) { + valueDTO.setProperty(entry.getKey(), primitive.getAsString()); + } + } + + /** + * @return the list of field names of EBusValueDTO + */ + @SuppressWarnings("unused") + private ArrayList getFields() { + ArrayList fields = new ArrayList<>(); + for (Field field : EBusValueDTO.class.getDeclaredFields()) { + + + @Nullable SerializedName annotation = field.getAnnotation(SerializedName.class); + if (annotation != null) { + fields.add(annotation.value()); + + } else { + fields.add(field.getName()); + } + } + return fields; + } + } \ No newline at end of file diff --git a/src/main/java/de/csdev/ebus/command/EBusCommand.java b/src/main/java/de/csdev/ebus/command/EBusCommand.java index d51b43b..80c1687 100644 --- a/src/main/java/de/csdev/ebus/command/EBusCommand.java +++ b/src/main/java/de/csdev/ebus/command/EBusCommand.java @@ -132,12 +132,12 @@ public void setLabel(@Nullable String label) { } public void setParentCollection(IEBusCommandCollection parentCollection) { - Objects.requireNonNull(parentCollection, "parentCollection"); + Objects.requireNonNull(parentCollection, "parentCollection must not be null"); this.parentCollection = parentCollection; } public void setProperties(Map properties) { - Objects.requireNonNull(properties, "properties"); + Objects.requireNonNull(properties, "properties must not be null"); HashMap props = new HashMap<>(); props.putAll(properties); this.properties = props; @@ -145,8 +145,8 @@ public void setProperties(Map properties) { public void setProperty(String key, String value) { - Objects.requireNonNull(key, "key"); - Objects.requireNonNull(value, "value"); + Objects.requireNonNull(key, "key must not be null"); + Objects.requireNonNull(value, "value must not be null"); this.properties = CollectionUtils.newMapIfNull(this.properties); diff --git a/src/main/java/de/csdev/ebus/command/EBusCommandRegistry.java b/src/main/java/de/csdev/ebus/command/EBusCommandRegistry.java index 2a6c1f2..c2aa824 100644 --- a/src/main/java/de/csdev/ebus/command/EBusCommandRegistry.java +++ b/src/main/java/de/csdev/ebus/command/EBusCommandRegistry.java @@ -19,7 +19,6 @@ import java.util.Map; import java.util.Objects; -import org.apache.commons.lang3.ObjectUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; @@ -33,6 +32,7 @@ import de.csdev.ebus.core.EBusConsts; import de.csdev.ebus.utils.CollectionUtils; import de.csdev.ebus.utils.EBusUtils; +import de.csdev.ebus.utils.ObjectUtil; /** * @author Christian Sowada - Initial contribution @@ -147,7 +147,7 @@ public void addCommandCollection(IEBusCommandCollection collection) { ByteBuffer buffer = ByteBuffer.wrap(data); if (buffer == null) { - return CollectionUtils.emptyList(); + return CollectionUtils.emptyList(); } return find(buffer); @@ -255,9 +255,9 @@ public EBusTypeRegistry getTypeRegistry() { @SuppressWarnings("java:S3776") public boolean matchesCommand(@NonNull IEBusCommandMethod command, @NonNull ByteBuffer data) { - Byte sourceAddress = ObjectUtils.defaultIfNull(command.getSourceAddress(), Byte.valueOf((byte) 0x00)); + Byte sourceAddress = ObjectUtil.defaultIfNull(command.getSourceAddress(), Byte.valueOf((byte) 0x00)); - Byte targetAddress = ObjectUtils.defaultIfNull(command.getDestinationAddress(), + Byte targetAddress = ObjectUtil.defaultIfNull(command.getDestinationAddress(), Byte.valueOf((byte) 0x00)); // fast check - is this the right telegram type? diff --git a/src/main/java/de/csdev/ebus/command/EBusCommandUtils.java b/src/main/java/de/csdev/ebus/command/EBusCommandUtils.java index 6e4ef3d..a737076 100644 --- a/src/main/java/de/csdev/ebus/command/EBusCommandUtils.java +++ b/src/main/java/de/csdev/ebus/command/EBusCommandUtils.java @@ -16,7 +16,6 @@ import java.util.Map.Entry; import java.util.Objects; -import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; @@ -32,6 +31,7 @@ import de.csdev.ebus.core.EBusReceiveStateMachine; import de.csdev.ebus.core.EBusReceiveStateMachine.State; import de.csdev.ebus.utils.EBusUtils; +import de.csdev.ebus.utils.StringUtil; /** * @author Christian Sowada - Initial contribution @@ -114,7 +114,7 @@ private EBusCommandUtils() { * @return */ public static byte unescapeSymbol(byte reversedByte) { - if (reversedByte == (byte) 0x00 ) { + if (reversedByte == (byte) 0x00) { return EBusConsts.ESCAPE; } return reversedByte == (byte) 0x01 ? EBusConsts.SYN : reversedByte; @@ -147,8 +147,9 @@ public static byte unescapeSymbol(byte reversedByte) { * @return * @throws EBusTypeException */ - public static @NonNull ByteBuffer buildCompleteTelegram(byte source, byte target, byte[] command, byte[] masterData, - byte[] slaveData) { + public static @NonNull ByteBuffer buildCompleteTelegram(byte source, byte target, byte[] command, + byte @Nullable [] masterData, + byte @Nullable [] slaveData) { boolean isMastereMaster = EBusUtils.isMasterAddress(target); boolean isBroadcast = target == EBusConsts.BROADCAST_ADDRESS; @@ -183,7 +184,8 @@ public static byte unescapeSymbol(byte reversedByte) { } /** - * Builds an escaped master telegram part or if slaveData is used a complete telegram incl. master ACK and SYN + * Builds an escaped master telegram part or if slaveData is used a complete + * telegram incl. master ACK and SYN * * @param source * @param target @@ -192,8 +194,8 @@ public static byte unescapeSymbol(byte reversedByte) { * @return * @throws EBusTypeException */ - public static @NonNull ByteBuffer buildPartMasterTelegram(byte source, byte target, byte[] command, - byte[] masterData) { + public static @NonNull ByteBuffer buildPartMasterTelegram(byte source, byte target, byte @NonNull [] command, + byte @NonNull [] masterData) { ByteBuffer buf = ByteBuffer.allocate(50); @@ -260,94 +262,114 @@ public static byte unescapeSymbol(byte reversedByte) { } /** + * Process a nested value entry of the command + * + * @param nestedValue + * @param values + * @return + */ + private static byte @Nullable [] processNestedValue(IEBusNestedValue nestedValue, + @Nullable Map values) { + List<@NonNull IEBusValue> list = nestedValue.getChildren(); + int n = 0; + + for (int i = 0; i < list.size(); i++) { + @Nullable + IEBusValue childValue = list.get(i); + if (values != null && values.containsKey(childValue.getName())) { + Boolean object = (Boolean) values.get(childValue.getName()); + if (object.booleanValue()) { + n = n | (1 << i); + } + } + } + + return new byte[] { (byte) n }; + } + + /** + * Process a single entry of the command + * + * @param entry + * @param type + * @param values + * @param buf + * @param complexTypes + * @return + * @throws EBusTypeException + */ + private static byte @Nullable [] processEntry(IEBusValue entry, IEBusType type, + @Nullable Map values, + ByteBuffer buf, Map> complexTypes) throws EBusTypeException { + + if (values != null && values.containsKey(entry.getName())) { + return type.encode(values.get(entry.getName())); + } + + if (type instanceof IEBusComplexType) { + complexTypes.put(buf.position(), (IEBusComplexType) type); + return new byte[entry.getType().getTypeLength()]; + } + + return type.encode(entry.getDefaultValue()); + } + + /** + * Process all complex types after the simple types are encoded + * + * @param buf + * @param complexTypes + * @throws EBusTypeException + */ + private static void processComplexTypes(ByteBuffer buf, Map> complexTypes) + throws EBusTypeException { + if (!complexTypes.isEmpty()) { + int orgPos = buf.position(); + buf.limit(buf.position()); + for (Entry> entry : complexTypes.entrySet()) { + buf.position(entry.getKey()); + buf.put(entry.getValue().encodeComplex(buf)); + } + buf.position(orgPos); + } + } + + /** + * Compose the master data part of a telegram + * * @param commandMethod * @param values * @return * @throws EBusTypeException */ - @SuppressWarnings("java:S3776") public static @NonNull ByteBuffer composeMasterData(@NonNull IEBusCommandMethod commandMethod, @Nullable Map values) throws EBusTypeException { Objects.requireNonNull(commandMethod); - ByteBuffer buf = ByteBuffer.allocate(50); - Map> complexTypes = new HashMap<>(); List<@NonNull IEBusValue> masterTypes = commandMethod.getMasterTypes(); if (masterTypes != null) { for (IEBusValue entry : masterTypes) { + byte @Nullable [] byteArray; - IEBusType type = entry.getType(); - byte[] b = null; - - // compute byte value from 8 bits if (entry instanceof IEBusNestedValue) { - IEBusNestedValue nestedValue = (IEBusNestedValue) entry; - List<@NonNull IEBusValue> list = nestedValue.getChildren(); - - int n = 0; - - for (int i = 0; i < list.size(); i++) { - IEBusValue childValue = list.get(i); - if (values != null && values.containsKey(childValue.getName())) { - Boolean object = (Boolean) values.get(childValue.getName()); - - if (object.booleanValue()) { - // set bit - n = n | (1 << i); - } - - } - } - - b = new byte[] { (byte) n }; - - } else if (values != null && values.containsKey(entry.getName())) { - // use the value from the values map if set - b = type.encode(values.get(entry.getName())); - + byteArray = processNestedValue((IEBusNestedValue) entry, values); } else { - if (type instanceof IEBusComplexType) { - - // add the complex to the list for post processing - complexTypes.put(buf.position(), (IEBusComplexType) type); - - // add placeholder - b = new byte[entry.getType().getTypeLength()]; - - } else { - b = type.encode(entry.getDefaultValue()); - - } - + byteArray = processEntry(entry, entry.getType(), values, buf, complexTypes); } - if (b == null) { - throw new EBusTypeException("Encoded value is null! " + type.toString()); + if (byteArray == null) { + throw new EBusTypeException("Encoded value is null! " + entry.getType().toString()); } - buf.put(b); + buf.put(byteArray); } } - // replace the placeholders with the complex values - if (!complexTypes.isEmpty()) { - int orgPos = buf.position(); - buf.limit(buf.position()); - for (Entry> entry : complexTypes.entrySet()) { - // jump to position - buf.position(entry.getKey()); - // put new value - buf.put(entry.getValue().encodeComplex(buf)); - - } - buf.position(orgPos); - - } + processComplexTypes(buf, complexTypes); - // reset pos to zero and set the new limit buf.limit(buf.position()); buf.position(0); @@ -402,7 +424,7 @@ public static byte unescapeSymbol(byte reversedByte) { targetChecked = EBusConsts.BROADCAST_ADDRESS; if (logger.isWarnEnabled()) { logger.warn("Replace target address {} with valid broadcast address 0xFE !", - EBusUtils.toHexDumpString(target)); + EBusUtils.toHexDumpString(target)); } } } else if (commandMethod.getType().equals(Type.MASTER_MASTER)) { @@ -416,7 +438,7 @@ public static byte unescapeSymbol(byte reversedByte) { } else { if (logger.isWarnEnabled()) { logger.warn("Replace slave target address {} with valid master address {}!", - EBusUtils.toHexDumpString(target), EBusUtils.toHexDumpString(targetChecked)); + EBusUtils.toHexDumpString(target), EBusUtils.toHexDumpString(targetChecked)); } } } @@ -515,7 +537,7 @@ private static int decodeValueList(@Nullable List<@NonNull IEBusValue> values, b Object decode2 = child.getType().decode(src); - if (StringUtils.isNotEmpty(child.getName())) { + if (StringUtil.isNotEmpty(child.getName())) { decode2 = applyNumberOperations(decode2, ev); result.put(child.getName(), decode2); } @@ -523,7 +545,7 @@ private static int decodeValueList(@Nullable List<@NonNull IEBusValue> values, b } } - if (StringUtils.isNotEmpty(ev.getName())) { + if (StringUtil.isNotEmpty(ev.getName())) { decode = applyNumberOperations(decode, ev); result.put(ev.getName(), decode); } diff --git a/src/main/java/de/csdev/ebus/command/EBusCommandValue.java b/src/main/java/de/csdev/ebus/command/EBusCommandValue.java index 51a2834..d80a914 100644 --- a/src/main/java/de/csdev/ebus/command/EBusCommandValue.java +++ b/src/main/java/de/csdev/ebus/command/EBusCommandValue.java @@ -76,10 +76,13 @@ protected EBusCommandValue createInstance() { clone.step = this.step; clone.type = this.type; - if (this.mapping != null) { - clone.mapping = new HashMap<>(); - for (Entry elem : this.mapping.entrySet()) { - clone.mapping.put(elem.getKey(), elem.getValue()); + Map tmpMapping = this.mapping; + + if (tmpMapping != null) { + Map cloneMapping = new HashMap<>(); + clone.mapping = cloneMapping; + for (Entry elem : tmpMapping.entrySet()) { + cloneMapping.put(elem.getKey(), elem.getValue()); } } diff --git a/src/main/java/de/csdev/ebus/command/IEBusValue.java b/src/main/java/de/csdev/ebus/command/IEBusValue.java index 58a8fd7..fa3e935 100644 --- a/src/main/java/de/csdev/ebus/command/IEBusValue.java +++ b/src/main/java/de/csdev/ebus/command/IEBusValue.java @@ -11,7 +11,6 @@ import java.math.BigDecimal; import java.util.Map; -import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/EBusAbstractType.java b/src/main/java/de/csdev/ebus/command/datatypes/EBusAbstractType.java index b1edb49..ed1c309 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/EBusAbstractType.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/EBusAbstractType.java @@ -17,8 +17,7 @@ import java.util.Objects; import java.util.TreeMap; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.reflect.FieldUtils; +import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; @@ -26,6 +25,8 @@ import de.csdev.ebus.core.EBusConsts; import de.csdev.ebus.utils.EBusUtils; +import de.csdev.ebus.utils.ArrayUtil; +import de.csdev.ebus.utils.FieldUtil; /** * @author Christian Sowada - Initial contribution @@ -34,7 +35,6 @@ @NonNullByDefault public abstract class EBusAbstractType implements IEBusType { - @SuppressWarnings({"null"}) private static final Logger logger = LoggerFactory.getLogger(EBusAbstractType.class); protected Map> otherInstances = new HashMap<>(); @@ -54,12 +54,11 @@ public abstract class EBusAbstractType implements IEBusType { */ protected byte @Nullable [] applyByteOrder(byte @Nullable [] data) { - // @SuppressWarnings({}) - data = ArrayUtils.clone(data); + data = ArrayUtil.clone(data); // reverse the byte order immutable if (reverseByteOrder) { - ArrayUtils.reverse(data); + ArrayUtil.reverse(data); } return data; @@ -73,8 +72,8 @@ public abstract class EBusAbstractType implements IEBusType { private @Nullable EBusAbstractType createNewInstance() { try { - @SuppressWarnings({ "unchecked" }) - EBusAbstractType newInstance = this.getClass().getDeclaredConstructor().newInstance(); + @SuppressWarnings("unchecked") + @Nullable EBusAbstractType newInstance = this.getClass().getDeclaredConstructor().newInstance(); if (newInstance != null) { newInstance.setTypesParent(types); return newInstance; @@ -131,17 +130,33 @@ public abstract class EBusAbstractType implements IEBusType { * @see de.csdev.ebus.command.datatypes.IEBusType#encode(java.lang.Object) */ @Override - public byte[] encode(@Nullable Object data) throws EBusTypeException { + public byte @NonNull [] encode(@Nullable Object data) throws EBusTypeException { // return the replace value if (data == null) { - return applyByteOrder(getReplaceValue()); + byte @Nullable [] tmpReplaceValue = getReplaceValue(); + if (tmpReplaceValue == null) { + throw new IllegalStateException("Replace value must not be null"); + } + + byte[] ndata = applyByteOrder(tmpReplaceValue); + if (ndata == null) { + throw new IllegalStateException("Replace value after applying byte order must not be null"); + } + + return ndata; } byte[] result = encodeInt(data); + if (result == null) { + throw new EBusTypeException("Encoded result must not be null"); + } // apply the right byte order after processing result = applyByteOrder(result); + if (result == null) { + throw new IllegalStateException("Result after applying byte order must not be null"); + } if (result.length != getTypeLength()) { throw new EBusTypeException("Result byte-array has size {0}, expected {1} for eBUS type {2}", result.length, @@ -261,8 +276,7 @@ protected void setInstanceProperty(@Nullable EBusAbstractType instance, @Null } try { - Field field = FieldUtils.getField(instance.getClass(), property, true); - + Field field = FieldUtil.getField(instance.getClass(), property, true); if (field != null) { field.set(instance, value); } diff --git a/src/main/java/de/csdev/ebus/command/datatypes/EBusTypeRegistry.java b/src/main/java/de/csdev/ebus/command/datatypes/EBusTypeRegistry.java index 5dfc0e4..de73933 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/EBusTypeRegistry.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/EBusTypeRegistry.java @@ -46,6 +46,8 @@ */ public class EBusTypeRegistry { + private static final String NO_E_BUS_DATA_TYPE_WITH_NAME = "No eBUS data type with name {} !"; + private static final Logger logger = LoggerFactory.getLogger(EBusTypeRegistry.class); private Map> types = null; @@ -133,7 +135,7 @@ protected void init() throws EBusTypeException { IEBusType eBusType = (IEBusType) types.get(type); if (eBusType == null) { - logger.warn("No eBUS data type with name {} !", type); + logger.warn(NO_E_BUS_DATA_TYPE_WITH_NAME, type); return null; } @@ -145,7 +147,7 @@ protected void init() throws EBusTypeException { * * @return */ - public Set getTypesNames() { + public Set getTypesNames() { return types.keySet(); } @@ -162,7 +164,7 @@ public Set getTypesNames() { IEBusType eBusType = types.get(type); if (eBusType == null) { - logger.warn("No eBUS data type with name {} !", type); + logger.warn(NO_E_BUS_DATA_TYPE_WITH_NAME, type); return null; } @@ -183,7 +185,7 @@ public Set getTypesNames() { IEBusType eBusType = (IEBusType) types.get(type); if (eBusType == null) { - logger.warn("No eBUS data type with name {} !", type); + logger.warn(NO_E_BUS_DATA_TYPE_WITH_NAME, type); return null; } diff --git a/src/main/java/de/csdev/ebus/command/datatypes/IEBusType.java b/src/main/java/de/csdev/ebus/command/datatypes/IEBusType.java index 174e263..1ced655 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/IEBusType.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/IEBusType.java @@ -44,14 +44,14 @@ public interface IEBusType { * @return * @throws EBusTypeException */ - public byte[] encode(@Nullable Object data) throws EBusTypeException; + public byte @Nullable [] encode(@Nullable Object data) throws EBusTypeException; /** * Returns the support types of this type * * @return */ - public String[] getSupportedTypes(); + public String [] getSupportedTypes(); /** * Internal only diff --git a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeBytes.java b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeBytes.java index 00c0f4f..09f080e 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeBytes.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeBytes.java @@ -25,15 +25,15 @@ @NonNullByDefault public class EBusTypeBytes extends EBusAbstractType { - public static String TYPE_BYTES = "bytes"; + public static final String TYPE_BYTES = "bytes"; - private static String[] supportedTypes = new String[] { TYPE_BYTES }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_BYTES }; private Integer length = 1; @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override diff --git a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeDate.java b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeDate.java index ef37a5b..c666ffa 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeDate.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeDate.java @@ -14,7 +14,6 @@ import java.util.GregorianCalendar; import java.util.Objects; -import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -33,25 +32,25 @@ @NonNullByDefault public class EBusTypeDate extends EBusAbstractType { - public static String TYPE_DATE = "date"; + public static final String TYPE_DATE = "date"; - public static String DEFAULT = "std"; // BDA - 4 + public static final String DEFAULT = "std"; // BDA - 4 - public static String SHORT = "short"; // BDA:3 - 3 + public static final String SHORT = "short"; // BDA:3 - 3 - public static String HEX = "hex"; // BDA:3 - 4 + public static final String HEX = "hex"; // BDA:3 - 4 - public static String HEX_SHORT = "hex_short"; // BDA:3 - 3 + public static final String HEX_SHORT = "hex_short"; // BDA:3 - 3 - public static String DAYS = "days"; // DAY - 2 + public static final String DAYS = "days"; // DAY - 2 - private static String[] supportedTypes = new String[] { TYPE_DATE }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_DATE }; private String variant = DEFAULT; @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override @@ -99,27 +98,27 @@ public int getTypeLength() { String.format("Input byte array must have a length of %d bytes!", getTypeLength())); } - if (StringUtils.equals(variant, SHORT)) { + if (SHORT.equals(variant)) { day = bcdType.decode(new byte[] { data[0] }); month = bcdType.decode(new byte[] { data[1] }); year = bcdType.decode(new byte[] { data[2] }); - } else if (StringUtils.equals(variant, DEFAULT)) { + } else if (DEFAULT.equals(variant)) { day = bcdType.decode(new byte[] { data[0] }); month = bcdType.decode(new byte[] { data[1] }); year = bcdType.decode(new byte[] { data[3] }); - } else if (StringUtils.equals(variant, HEX_SHORT)) { + } else if (HEX_SHORT.equals(variant)) { day = charType.decode(new byte[] { data[0] }); month = charType.decode(new byte[] { data[1] }); year = charType.decode(new byte[] { data[2] }); - } else if (StringUtils.equals(variant, HEX)) { + } else if (HEX.equals(variant)) { day = charType.decode(new byte[] { data[0] }); month = charType.decode(new byte[] { data[1] }); year = charType.decode(new byte[] { data[3] }); - } else if (StringUtils.equals(variant, DAYS)) { + } else if (DAYS.equals(variant)) { BigDecimal daysSince1900 = wordType.decode(data); if (daysSince1900 == null) { @@ -192,7 +191,7 @@ public int getTypeLength() { calendar.set(Calendar.MILLISECOND, 0); - if (StringUtils.equals(variant, DEFAULT)) { + if (DEFAULT.equals(variant)) { int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); dayOfWeek = dayOfWeek == 1 ? 7 : dayOfWeek - 1; @@ -201,13 +200,13 @@ public int getTypeLength() { bcdType.encode(calendar.get(Calendar.MONTH) + 1)[0], bcdType.encode(dayOfWeek)[0], bcdType.encode(calendar.get(Calendar.YEAR) % 100)[0] }; - } else if (StringUtils.equals(variant, SHORT)) { + } else if (SHORT.equals(variant)) { result = new byte[] { bcdType.encode(calendar.get(Calendar.DAY_OF_MONTH))[0], bcdType.encode(calendar.get(Calendar.MONTH) + 1)[0], bcdType.encode(calendar.get(Calendar.YEAR) % 100)[0] }; - } else if (StringUtils.equals(variant, HEX)) { + } else if (HEX.equals(variant)) { int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); dayOfWeek = dayOfWeek == 1 ? 7 : dayOfWeek - 1; @@ -216,13 +215,13 @@ public int getTypeLength() { charType.encode(calendar.get(Calendar.MONTH) + 1)[0], charType.encode(dayOfWeek)[0], charType.encode(calendar.get(Calendar.YEAR) % 100)[0] }; - } else if (StringUtils.equals(variant, HEX_SHORT)) { + } else if (HEX_SHORT.equals(variant)) { result = new byte[] { charType.encode(calendar.get(Calendar.DAY_OF_MONTH))[0], charType.encode(calendar.get(Calendar.MONTH) + 1)[0], charType.encode(calendar.get(Calendar.YEAR) % 100)[0] }; - } else if (StringUtils.equals(variant, DAYS)) { + } else if (DAYS.equals(variant)) { long millis = calendar.getTimeInMillis(); diff --git a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeDateTime.java b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeDateTime.java index 1088965..86e2e6f 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeDateTime.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeDateTime.java @@ -14,7 +14,6 @@ import java.util.HashMap; import java.util.Map; -import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -25,6 +24,7 @@ import de.csdev.ebus.command.datatypes.EBusTypeException; import de.csdev.ebus.command.datatypes.IEBusType; import de.csdev.ebus.utils.EBusDateTime; +import de.csdev.ebus.utils.ArrayUtil; /** * @author Christian Sowada - Initial contribution @@ -35,15 +35,15 @@ public class EBusTypeDateTime extends EBusAbstractType { private static final Logger logger = LoggerFactory.getLogger(EBusTypeDateTime.class); - public static String TYPE_DATETIME = "datetime"; + public static final String TYPE_DATETIME = "datetime"; - private static String[] supportedTypes = new String[] { TYPE_DATETIME }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_DATETIME }; - public static String TIME_FIRST = "timeFirst"; + public static final String TIME_FIRST = "timeFirst"; - public static String VARIANT_DATE = "variantDate"; + public static final String VARIANT_DATE = "variantDate"; - public static String VARIANT_TIME = "variantTime"; + public static final String VARIANT_TIME = "variantTime"; private boolean timeFirst = true; @@ -56,7 +56,7 @@ public class EBusTypeDateTime extends EBusAbstractType { if (reverseByteOrder) { logger.warn("Parameter 'reverseByteOrder' not supported for EBusTypeDateTime yet!"); } - return ArrayUtils.clone(data); + return ArrayUtil.clone(data); } @Override @@ -160,7 +160,7 @@ public byte[] encodeInt(@Nullable Object data) throws EBusTypeException { @Override public String @NonNull [] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } private @Nullable IEBusType getTimeType() { diff --git a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeKWCrc.java b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeKWCrc.java index 7e8e59e..c833069 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeKWCrc.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeKWCrc.java @@ -26,13 +26,13 @@ @NonNullByDefault public class EBusTypeKWCrc extends EBusAbstractType implements IEBusComplexType { - public static String TYPE_KW_CRC = "kw-crc"; + public static final String TYPE_KW_CRC = "kw-crc"; - public static String POS = "pos"; + public static final String POS = "pos"; public static int pos = 0; - private static String[] supportedTypes = new String[] { TYPE_KW_CRC }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_KW_CRC }; public EBusTypeKWCrc() { replaceValue = new byte[] { (byte) 0xCC }; @@ -45,7 +45,7 @@ public int getTypeLength() { @Override public String @NonNull [] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override diff --git a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeMultiWord.java b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeMultiWord.java index a2cf38c..0c82f91 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeMultiWord.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeMultiWord.java @@ -27,20 +27,19 @@ @NonNullByDefault public class EBusTypeMultiWord extends EBusAbstractType { - public static String TYPE_MWORD = "mword"; + public static final String TYPE_MWORD = "mword"; - public static String BLOCK_MULTIPLIER = "multiplier"; + public static final String BLOCK_MULTIPLIER = "multiplier"; - private static String[] supportedTypes = new String[] { TYPE_MWORD }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_MWORD }; private int length = 2; - @SuppressWarnings({"null"}) private BigDecimal multiplier = BigDecimal.valueOf(1000); @Override public String @NonNull [] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override @@ -85,9 +84,9 @@ public byte[] encodeInt(@Nullable Object data) throws EBusTypeException { byte[] result = new byte[getTypeLength()]; - int length = this.length - 1; + int tmpLength = this.length - 1; - for (int i = length; i >= 0; i--) { + for (int i = tmpLength; i >= 0; i--) { BigDecimal factor = this.multiplier.pow(i); BigDecimal[] divideAndRemainder = value.divideAndRemainder(factor); diff --git a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeString.java b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeString.java index 156a660..1001a07 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeString.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeString.java @@ -20,15 +20,15 @@ @NonNullByDefault public class EBusTypeString extends EBusAbstractType { - public static String TYPE_STRING = "string"; + public static final String TYPE_STRING = "string"; - private static String[] supportedTypes = new String[] { TYPE_STRING }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_STRING }; private Integer length = 1; @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override diff --git a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeTime.java b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeTime.java index b81015e..ea0fdda 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeTime.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeTime.java @@ -13,7 +13,6 @@ import java.util.Calendar; import java.util.GregorianCalendar; -import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -33,29 +32,28 @@ @NonNullByDefault public class EBusTypeTime extends EBusAbstractType { - public static String TYPE_TIME = "time"; + public static final String TYPE_TIME = "time"; - public static String DEFAULT = "std"; // BTI - 3 - public static String HEX = "hex"; // HTI - 3 + public static final String DEFAULT = "std"; // BTI - 3 + public static final String HEX = "hex"; // HTI - 3 - public static String SHORT = "short"; // BTM - 2 - public static String HEX_SHORT = "hex_short"; // HTM - 2 + public static final String SHORT = "short"; // BTM - 2 + public static final String HEX_SHORT = "hex_short"; // HTM - 2 - public static String MINUTES = "minutes"; // MIN - 2 - public static String MINUTES_SHORT = "minutes_short"; // MIN - 1 + public static final String MINUTES = "minutes"; // MIN - 2 + public static final String MINUTES_SHORT = "minutes_short"; // MIN - 1 - private static String[] supportedTypes = new String[] { TYPE_TIME }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_TIME }; - public static String MINUTE_MULTIPLIER = "minuteMultiplier"; + public static final String MINUTE_MULTIPLIER = "minuteMultiplier"; private String variant = DEFAULT; - @SuppressWarnings({"null"}) private BigDecimal minuteMultiplier = BigDecimal.valueOf(1); @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override @@ -101,28 +99,28 @@ public EBusDateTime decodeInt(byte @Nullable [] data) throws EBusTypeException { throw new EBusTypeException("Unable to get all required EBusTyp's type!"); } - if (StringUtils.equals(variant, SHORT)) { + if (SHORT.equals(variant)) { minute = bcdType.decode(new byte[] { data[0] }); hour = bcdType.decode(new byte[] { data[1] }); - } else if (StringUtils.equals(variant, DEFAULT)) { + } else if (DEFAULT.equals(variant)) { second = bcdType.decode(new byte[] { data[0] }); minute = bcdType.decode(new byte[] { data[1] }); hour = bcdType.decode(new byte[] { data[2] }); - } else if (StringUtils.equals(variant, HEX)) { + } else if (HEX.equals(variant)) { second = charType.decode(new byte[] { data[0] }); minute = charType.decode(new byte[] { data[1] }); hour = charType.decode(new byte[] { data[2] }); - } else if (StringUtils.equals(variant, HEX_SHORT)) { + } else if (HEX_SHORT.equals(variant)) { minute = charType.decode(new byte[] { data[0] }); hour = charType.decode(new byte[] { data[1] }); - } else if (StringUtils.equals(variant, MINUTES) || StringUtils.equals(variant, MINUTES_SHORT)) { + } else if (MINUTES.equals(variant) || MINUTES_SHORT.equals(variant)) { BigDecimal minutesSinceMidnight = null; - if (StringUtils.equals(variant, MINUTES_SHORT)) { + if (MINUTES_SHORT.equals(variant)) { IEBusType type = types.getType(EBusTypeUnsignedNumber.TYPE_UNUMBER, IEBusType.LENGTH, 1); if (type == null) { throw new EBusTypeException("Unable to get required EBusTyp type!"); @@ -192,29 +190,29 @@ public EBusDateTime decodeInt(byte @Nullable [] data) throws EBusTypeException { calendar.set(1970, 0, 1); - if (StringUtils.equals(variant, DEFAULT)) { + if (DEFAULT.equals(variant)) { result = new byte[] { bcdType.encode(calendar.get(Calendar.SECOND))[0], bcdType.encode(calendar.get(Calendar.MINUTE))[0], bcdType.encode(calendar.get(Calendar.HOUR_OF_DAY))[0] }; - } else if (StringUtils.equals(variant, SHORT)) { + } else if (SHORT.equals(variant)) { result = new byte[] { bcdType.encode(calendar.get(Calendar.MINUTE))[0], bcdType.encode(calendar.get(Calendar.HOUR_OF_DAY))[0] }; - } else if (StringUtils.equals(variant, HEX)) { + } else if (HEX.equals(variant)) { result = new byte[] { charType.encode(calendar.get(Calendar.SECOND))[0], charType.encode(calendar.get(Calendar.MINUTE))[0], charType.encode(calendar.get(Calendar.HOUR_OF_DAY))[0] }; - } else if (StringUtils.equals(variant, HEX_SHORT)) { + } else if (HEX_SHORT.equals(variant)) { result = new byte[] { charType.encode(calendar.get(Calendar.MINUTE))[0], charType.encode(calendar.get(Calendar.HOUR_OF_DAY))[0] }; - } else if (StringUtils.equals(variant, MINUTES) || StringUtils.equals(variant, MINUTES_SHORT)) { + } else if (MINUTES.equals(variant) || MINUTES_SHORT.equals(variant)) { long millis = calendar.getTimeInMillis(); @@ -232,7 +230,7 @@ public EBusDateTime decodeInt(byte @Nullable [] data) throws EBusTypeException { // xxx minutes = minutes.divide(minuteMultiplier, 0, RoundingMode.HALF_UP); - if (StringUtils.equals(variant, MINUTES_SHORT)) { + if (MINUTES_SHORT.equals(variant)) { result = charType.encode(minutes); } else { result = wordType.encode(minutes); diff --git a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeVersion.java b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeVersion.java index f1deb89..9635e77 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeVersion.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/ext/EBusTypeVersion.java @@ -25,13 +25,13 @@ @NonNullByDefault public class EBusTypeVersion extends EBusAbstractType { - public static String TYPE_VERSION = "version"; + public static final String TYPE_VERSION = "version"; - private static String[] supportedTypes = new String[] { TYPE_VERSION }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_VERSION }; @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/AbstractEBusTypeNumber.java b/src/main/java/de/csdev/ebus/command/datatypes/std/AbstractEBusTypeNumber.java index ce118a2..0937e1e 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/AbstractEBusTypeNumber.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/AbstractEBusTypeNumber.java @@ -11,13 +11,13 @@ import java.math.BigDecimal; import java.math.BigInteger; -import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import de.csdev.ebus.command.datatypes.EBusAbstractType; import de.csdev.ebus.command.datatypes.EBusTypeException; import de.csdev.ebus.utils.NumberUtils; +import de.csdev.ebus.utils.ArrayUtil; @NonNullByDefault public abstract class AbstractEBusTypeNumber extends EBusAbstractType { @@ -38,8 +38,8 @@ public byte[] getReplaceValue() { @Override public @Nullable BigDecimal decodeInt(byte @Nullable [] data) throws EBusTypeException { - byte[] clone = ArrayUtils.clone(data); - ArrayUtils.reverse(clone); + byte[] clone = ArrayUtil.clone(data); + ArrayUtil.reverse(clone); return new BigDecimal(new BigInteger(clone)); } diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/AbstractEBusTypeUnsignedNumber.java b/src/main/java/de/csdev/ebus/command/datatypes/std/AbstractEBusTypeUnsignedNumber.java index eed1c2e..73a43b1 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/AbstractEBusTypeUnsignedNumber.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/AbstractEBusTypeUnsignedNumber.java @@ -11,13 +11,13 @@ import java.math.BigDecimal; import java.math.BigInteger; -import org.apache.commons.lang3.ArrayUtils; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import de.csdev.ebus.command.datatypes.EBusAbstractType; import de.csdev.ebus.command.datatypes.EBusTypeException; import de.csdev.ebus.utils.NumberUtils; +import de.csdev.ebus.utils.ArrayUtil; /** * @author Christian Sowada - Initial contribution @@ -29,8 +29,8 @@ public abstract class AbstractEBusTypeUnsignedNumber extends EBusAbstractType { - public static String TYPE_BCD = "bcd"; + public static final String TYPE_BCD = "bcd"; private static final String[] supportedTypes = new String[] { TYPE_BCD }; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeBit.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeBit.java index fa661d2..52ced40 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeBit.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeBit.java @@ -28,7 +28,7 @@ public class EBusTypeBit extends EBusAbstractType { private static final String[] supportedTypes = new String[] { TYPE_BIT }; - public static String POS = "pos"; + public static final String POS = "pos"; // will be filled by reflection @SuppressWarnings({"null", "java:S1845"}) diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeByte.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeByte.java index a40ce12..d6b62d6 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeByte.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeByte.java @@ -19,8 +19,8 @@ @NonNullByDefault public class EBusTypeByte extends AbstractEBusTypeUnsignedNumber { - public static String TYPE_UCHAR = "uchar"; - public static String TYPE_BYTE = "byte"; + public static final String TYPE_UCHAR = "uchar"; + public static final String TYPE_BYTE = "byte"; private static String[] supportedTypes = new String[] { TYPE_BYTE, TYPE_UCHAR }; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeChar.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeChar.java index 64ea5e8..a697cb3 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeChar.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeChar.java @@ -19,7 +19,7 @@ @NonNullByDefault public class EBusTypeChar extends AbstractEBusTypeNumber { - public static String TYPE_CHAR = "char"; + public static final String TYPE_CHAR = "char"; private static String[] supportedTypes = new String[] { TYPE_CHAR }; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData1b.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData1b.java index 70471f6..deb823e 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData1b.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData1b.java @@ -19,7 +19,7 @@ @NonNullByDefault public class EBusTypeData1b extends AbstractEBusTypeNumber { - public static String TYPE_DATA1B = "data1b"; + public static final String TYPE_DATA1B = "data1b"; private static String[] supportedTypes = new String[] { TYPE_DATA1B }; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData1c.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData1c.java index 65efbdd..37e6356 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData1c.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData1c.java @@ -24,7 +24,7 @@ @NonNullByDefault public class EBusTypeData1c extends AbstractEBusTypeUnsignedNumber { - public static String TYPE_DATA1C = "data1c"; + public static final String TYPE_DATA1C = "data1c"; private static String[] supportedTypes = new String[] { TYPE_DATA1C }; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData2b.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData2b.java index c3fe23e..85ae0dd 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData2b.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData2b.java @@ -24,7 +24,7 @@ @NonNullByDefault public class EBusTypeData2b extends AbstractEBusTypeNumber { - public static String TYPE_DATA2B = "data2b"; + public static final String TYPE_DATA2B = "data2b"; private static String[] supportedTypes = new String[] { TYPE_DATA2B }; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData2c.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData2c.java index 434c42a..190df6c 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData2c.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeData2c.java @@ -24,7 +24,7 @@ @NonNullByDefault public class EBusTypeData2c extends AbstractEBusTypeNumber { - public static String TYPE_DATA2C = "data2c"; + public static final String TYPE_DATA2C = "data2c"; private static String[] supportedTypes = new String[] { TYPE_DATA2C }; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeFloat.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeFloat.java index f1b3955..9a0a913 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeFloat.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeFloat.java @@ -29,10 +29,9 @@ @NonNullByDefault public class EBusTypeFloat extends EBusAbstractType { - @SuppressWarnings({"null"}) private static final Logger logger = LoggerFactory.getLogger(EBusTypeFloat.class); - public static String TYPE_FLOAT = "float"; + public static final String TYPE_FLOAT = "float"; private static String[] supportedTypes = new String[] { TYPE_FLOAT }; diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeInteger.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeInteger.java index a834bdb..d200bc3 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeInteger.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeInteger.java @@ -19,13 +19,13 @@ @NonNullByDefault public class EBusTypeInteger extends AbstractEBusTypeNumber { - public static String TYPE_INTEGER = "int"; + public static final String TYPE_INTEGER = "int"; - private static String[] supportedTypes = new String[] { TYPE_INTEGER }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_INTEGER }; @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeNumber.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeNumber.java index 075cc01..5479210 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeNumber.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeNumber.java @@ -19,15 +19,15 @@ @NonNullByDefault public class EBusTypeNumber extends AbstractEBusTypeNumber { - public static String TYPE_NUMBER = "number"; + public static final String TYPE_NUMBER = "number"; - private static String[] supportedTypes = new String[] { TYPE_NUMBER }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_NUMBER }; private int length = 1; @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeUnsignedNumber.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeUnsignedNumber.java index 7c0bb3a..820efc8 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeUnsignedNumber.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeUnsignedNumber.java @@ -19,15 +19,15 @@ @NonNullByDefault public class EBusTypeUnsignedNumber extends AbstractEBusTypeUnsignedNumber { - public static String TYPE_UNUMBER = "unumber"; + public static final String TYPE_UNUMBER = "unumber"; - private static String[] supportedTypes = new String[] { TYPE_UNUMBER }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_UNUMBER }; private int length = 1; @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override diff --git a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeWord.java b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeWord.java index 85fe808..bf1f5b0 100644 --- a/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeWord.java +++ b/src/main/java/de/csdev/ebus/command/datatypes/std/EBusTypeWord.java @@ -19,14 +19,14 @@ @NonNullByDefault public class EBusTypeWord extends AbstractEBusTypeUnsignedNumber { - public static String TYPE_WORD = "word"; - public static String TYPE_UINT = "uint"; + public static final String TYPE_WORD = "word"; + public static final String TYPE_UINT = "uint"; - private static String[] supportedTypes = new String[] { TYPE_WORD, TYPE_UINT }; + private static final String[] SUPPORTED_TYPES = new String[] { TYPE_WORD, TYPE_UINT }; @Override public String[] getSupportedTypes() { - return supportedTypes; + return SUPPORTED_TYPES; } @Override diff --git a/src/main/java/de/csdev/ebus/core/EBusControllerBase.java b/src/main/java/de/csdev/ebus/core/EBusControllerBase.java index 12ba704..9538419 100644 --- a/src/main/java/de/csdev/ebus/core/EBusControllerBase.java +++ b/src/main/java/de/csdev/ebus/core/EBusControllerBase.java @@ -24,16 +24,31 @@ import org.slf4j.LoggerFactory; /** - * @author Christian Sowada - Initial contribution + * Base implementation of an eBus controller that handles the core functionality + * of eBus communication, including event handling, connection management, + * and telegram processing. + * + * This class provides: + * - Event listener management + * - Thread pool management for event processing + * - Connection status handling + * - Watchdog timer functionality + * - Queue management for sending/receiving telegrams * + * @author Christian Sowada - Initial contribution */ public abstract class EBusControllerBase extends Thread implements IEBusController { private static final Logger logger = LoggerFactory.getLogger(EBusControllerBase.class); private static final String THREADPOOL_NOT_READY = "ThreadPool not ready!"; + private static final int DEFAULT_CORE_POOL_SIZE = 5; + private static final int MAX_POOL_SIZE = 60; + private static final long KEEP_ALIVE_TIME = 60L; + private static final int DEFAULT_WATCHDOG_TIMEOUT = 300; // 5 minutes + private static final int THREAD_POOL_TERMINATION_TIMEOUT = 10; - /** serial receive buffer */ + /** Serial receive buffer for processing incoming data */ protected @NonNull EBusReceiveStateMachine machine = new EBusReceiveStateMachine(); /** the list for listeners */ @@ -46,51 +61,72 @@ public abstract class EBusControllerBase extends Thread implements IEBusControll private ScheduledFuture watchdogTimer; - private int watchdogTimerTimeout = 300; // 5min + private int watchdogTimerTimeout = DEFAULT_WATCHDOG_TIMEOUT; // 5min protected @NonNull EBusQueue queue = new EBusQueue(); private @NonNull ConnectionStatus connectionStatus = ConnectionStatus.DISCONNECTED; - /* - * (non-Javadoc) - * - * @see de.csdev.ebus.core.IEBusController#addToSendQueue(byte[], int) + /** + * Adds a telegram to the send queue with a specified maximum number of retry attempts. + * + * @param buffer The telegram data to send + * @param maxAttempts The maximum number of retry attempts + * @return A unique identifier for the queued telegram + * @throws EBusControllerException if the controller is not connected or the queue operation fails + * @throws NullPointerException if buffer is null */ @Override - public @NonNull Integer addToSendQueue(final byte @NonNull [] buffer, final int maxAttemps) throws EBusControllerException { - if (getConnectionStatus() != ConnectionStatus.CONNECTED) { - throw new EBusControllerException("Controller not connected, unable to add telegrams to send queue!"); + public @NonNull Integer addToSendQueue(final byte @NonNull [] buffer, final int maxAttempts) throws EBusControllerException { + Objects.requireNonNull(buffer, "buffer cannot be null"); + if (maxAttempts <= 0) { + throw new IllegalArgumentException("maxAttempts must be greater than 0"); } - - Integer sendId = queue.addToSendQueue(buffer, maxAttemps); - + + validateConnectionStatus(); + + Integer sendId = queue.addToSendQueue(buffer, maxAttempts); if (sendId == null) { - throw new EBusControllerException("Unable to add telegrams to send queue!"); + throw new EBusControllerException("Failed to add telegram to send queue"); } - + + logger.debug("Added telegram to send queue with ID {} and {} max attempts", sendId, maxAttempts); return sendId; } - /* - * (non-Javadoc) - * - * @see de.csdev.ebus.core.IEBusController#addToSendQueue(byte[]) + /** + * Adds a telegram to the send queue using default retry attempts. + * + * @param buffer The telegram data to send + * @return A unique identifier for the queued telegram + * @throws EBusControllerException if the controller is not connected or the queue operation fails + * @throws NullPointerException if buffer is null */ @Override public @NonNull Integer addToSendQueue(final byte @NonNull [] buffer) throws EBusControllerException { - if (getConnectionStatus() != ConnectionStatus.CONNECTED) { - throw new EBusControllerException("Controller not connected, unable to add telegrams to send queue!"); - } - + Objects.requireNonNull(buffer, "buffer cannot be null"); + + validateConnectionStatus(); + Integer sendId = queue.addToSendQueue(buffer); - if (sendId == null) { - throw new EBusControllerException("Unable to add telegrams to send queue!"); + throw new EBusControllerException("Failed to add telegram to send queue"); } - + + logger.debug("Added telegram to send queue with ID {}", sendId); return sendId; } + + /** + * Validates that the controller is in a connected state. + * + * @throws EBusControllerException if the controller is not connected + */ + private void validateConnectionStatus() throws EBusControllerException { + if (getConnectionStatus() != ConnectionStatus.CONNECTED) { + throw new EBusControllerException("Controller is not connected - current status: " + getConnectionStatus()); + } + } /* * (non-Javadoc) @@ -115,95 +151,101 @@ public boolean removeEBusEventListener(final @NonNull IEBusConnectorEventListene } /** - * @param e + * Notifies all registered listeners about a connection exception. + * The notification is dispatched asynchronously via the thread pool. + * If the controller is not running or the thread pool is not available, + * the event will not be fired. + * + * @param exception The connection exception that occurred + * @throws NullPointerException if exception is null */ - protected void fireOnConnectionException(final @NonNull Exception e) { + protected void fireOnConnectionException(final @NonNull Exception exception) { + Objects.requireNonNull(exception, "exception cannot be null"); - Objects.requireNonNull(e); - - if (!isRunning()) { - return; - } - - if (threadPool == null || threadPool.isTerminated()) { - logger.warn(THREADPOOL_NOT_READY); + if (!isRunning() || !validateThreadPool()) { + logger.warn("Cannot fire connection exception event - controller not running or thread pool not ready"); return; } threadPool.execute(() -> { + String exceptionName = exception.getClass().getSimpleName(); + logger.debug("Firing connection exception event for {} to {} listeners", + exceptionName, listeners.size()); + for (IEBusConnectorEventListener listener : listeners) { - if (!Thread.interrupted()) { - try { - listener.onConnectionException(e); - } catch (Exception e1) { - logger.error("Error while firing onConnectionException events!", e1); - } + if (Thread.interrupted()) { + logger.debug("Connection exception event processing interrupted"); + break; + } + try { + listener.onConnectionException(exception); + } catch (Exception e) { + logger.error("Error in connection exception listener: {}", e.getMessage(), e); } } }); } /** - * Called if a valid eBus telegram was received. Send to event - * listeners via thread pool to prevent blocking. + * Fires a telegram received event to all registered listeners. + * The event is dispatched asynchronously via the thread pool to prevent blocking. + * Validates the state and data before firing the event. * - * @param receivedData - * @param sendQueueId + * @param receivedData The received telegram data, must not be null or empty + * @param sendQueueId The optional queue ID if this was a response to a sent telegram */ protected void fireOnEBusTelegramReceived(final byte @NonNull [] receivedData, final Integer sendQueueId) { + Objects.requireNonNull(receivedData, "receivedData cannot be null"); - if (!isRunning()) { - return; - } - - if (threadPool == null || threadPool.isTerminated()) { - logger.warn(THREADPOOL_NOT_READY + " Can't fire onTelegramReceived events ..."); - return; - } - - if (receivedData.length == 0) { - logger.warn("Telegram data is empty! Can't fire onTelegramReceived events ..."); + if (!isRunning() || !validateThreadPool() || receivedData.length == 0) { + logger.warn("Cannot fire telegram received event - controller not running or invalid data"); return; } threadPool.execute(() -> { for (IEBusConnectorEventListener listener : listeners) { - if (!Thread.interrupted()) { - try { - listener.onTelegramReceived(receivedData, sendQueueId); - } catch (Exception e) { - logger.error("Error while firing onTelegramReceived events!", e); - } + if (Thread.interrupted()) { + break; + } + try { + listener.onTelegramReceived(receivedData, sendQueueId); + } catch (Exception e) { + logger.error("Error in telegram received event listener", e); } } }); } /** - * @param exception - * @param sendQueueId + * Notifies all registered listeners about a data exception that occurred during + * telegram processing. The notification is dispatched asynchronously via the thread pool. + * + * @param exception The data exception that occurred + * @param sendQueueId The optional queue ID if this was related to a sent telegram + * @throws NullPointerException if exception is null */ protected void fireOnEBusDataException(final @NonNull EBusDataException exception, final Integer sendQueueId) { + Objects.requireNonNull(exception, "exception cannot be null"); - Objects.requireNonNull(exception); - - if (!isRunning()) { - return; - } - - if (threadPool == null || threadPool.isTerminated()) { - logger.warn(THREADPOOL_NOT_READY); + if (!isRunning() || !validateThreadPool()) { + logger.warn("Cannot fire data exception event - controller not running or thread pool not ready"); return; } threadPool.execute(() -> { + String exceptionMessage = exception.getMessage(); + logger.debug("Firing data exception event to {} listeners. Error: {}", + listeners.size(), exceptionMessage); + for (IEBusConnectorEventListener listener : listeners) { - if (!Thread.interrupted()) { - try { - listener.onTelegramException(exception, sendQueueId); - } catch (Exception e) { - logger.error("Error while firing onTelegramException events!", e); - } + if (Thread.interrupted()) { + logger.debug("Data exception event processing interrupted"); + break; + } + try { + listener.onTelegramException(exception, sendQueueId); + } catch (Exception e) { + logger.error("Error in data exception listener: {}", e.getMessage(), e); } } }); @@ -239,39 +281,47 @@ protected void fireOnEBusConnectionStatusChange(final @NonNull ConnectionStatus /** * */ + /** + * Initializes the thread pools used for event processing and watchdog timer. + * Creates a main thread pool for processing received telegrams and events, + * and a single-threaded scheduled executor for the watchdog timer. + */ protected void initThreadPool() { - // create new thread pool to send received telegrams - // limit the number of threads to 30 - threadPool = new ThreadPoolExecutor(5, 60, 60L, TimeUnit.SECONDS, new SynchronousQueue<>(), - new EBusWorkerThreadFactory("ebus-receiver", true)); - - // create watch dog thread pool - threadPoolWDT = Executors.newSingleThreadScheduledExecutor(new EBusWorkerThreadFactory("ebus-wdt", false)); + // Create new thread pool to send received telegrams + threadPool = new ThreadPoolExecutor( + DEFAULT_CORE_POOL_SIZE, + MAX_POOL_SIZE, + KEEP_ALIVE_TIME, + TimeUnit.SECONDS, + new SynchronousQueue<>(), + new EBusWorkerThreadFactory("ebus-receiver", true) + ); + + // Create watch dog thread pool + threadPoolWDT = Executors.newSingleThreadScheduledExecutor( + new EBusWorkerThreadFactory("ebus-wdt", false) + ); } /** - * @throws InterruptedException + * Shuts down both the main thread pool and watchdog timer thread pool. + * Attempts graceful shutdown first, then forces shutdown if needed. + * Waits for pool termination with a timeout. * + * @throws InterruptedException if interrupted while waiting for thread pools to terminate */ protected void shutdownThreadPool() throws InterruptedException { - // shutdown threadpool + // Shutdown main thread pool if (threadPool != null && !threadPool.isShutdown()) { threadPool.shutdownNow(); + threadPool.awaitTermination(THREAD_POOL_TERMINATION_TIMEOUT, TimeUnit.SECONDS); + threadPool = null; } + // Shutdown watchdog thread pool if (threadPoolWDT != null && !threadPoolWDT.isShutdown()) { threadPoolWDT.shutdownNow(); - } - - if (threadPool != null) { - // wait up to 10sec. for the thread pool - threadPool.awaitTermination(10, TimeUnit.SECONDS); - threadPool = null; - } - - if (threadPoolWDT != null) { - // wait up to 10sec. for the thread pool - threadPoolWDT.awaitTermination(10, TimeUnit.SECONDS); + threadPoolWDT.awaitTermination(THREAD_POOL_TERMINATION_TIMEOUT, TimeUnit.SECONDS); threadPoolWDT = null; } } @@ -286,49 +336,115 @@ public boolean isRunning() { return !isInterrupted() && isAlive(); } + /** + * Disposes of this controller instance, cleaning up all resources. + * This method: + * - Clears all event listeners + * - Cancels the watchdog timer + * - Shuts down thread pools + * - Resets internal state + * + * This method should be called when the controller is no longer needed. + * + * @throws InterruptedException if interrupted while shutting down thread pools + */ protected void dispose() throws InterruptedException { + logger.debug("Disposing eBus controller..."); + + synchronized (this) { + // Clear all listeners first to prevent new events during shutdown + if (!listeners.isEmpty()) { + logger.debug("Removing {} event listeners", listeners.size()); + listeners.clear(); + } - listeners.clear(); + // Cancel watchdog timer + if (watchdogTimer != null) { + logger.debug("Cancelling watchdog timer"); + watchdogTimer.cancel(true); + watchdogTimer = null; + } - if (watchdogTimer != null) { - watchdogTimer.cancel(true); - watchdogTimer = null; - } + // Reset connection status + connectionStatus = ConnectionStatus.DISCONNECTED; - shutdownThreadPool(); + // Clear the send queue + queue = new EBusQueue(); + + // Shutdown thread pools + logger.debug("Shutting down thread pools"); + shutdownThreadPool(); + } + + logger.debug("eBus controller disposed successfully"); } + /** + * Resets the watchdog timer with the current timeout value. + * Cancels any existing timer and schedules a new one if the thread pool is active. + * This method is thread-safe. + */ protected void resetWatchdogTimer() { - Runnable r = EBusControllerBase.this::fireWatchDogTimer; + synchronized (this) { + if (threadPoolWDT == null || threadPoolWDT.isShutdown()) { + logger.debug("Cannot reset watchdog timer - thread pool is not active"); + return; + } - if (watchdogTimer != null && !watchdogTimer.isCancelled()) { - watchdogTimer.cancel(true); - } + // Cancel existing timer if present + if (watchdogTimer != null && !watchdogTimer.isCancelled()) { + watchdogTimer.cancel(true); + } - if (!threadPoolWDT.isShutdown()) { - watchdogTimer = threadPoolWDT.schedule(r, watchdogTimerTimeout, TimeUnit.SECONDS); + // Schedule new timer + watchdogTimer = threadPoolWDT.schedule( + this::fireWatchDogTimer, + watchdogTimerTimeout, + TimeUnit.SECONDS + ); } - } - /* - * (non-Javadoc) - * - * @see de.csdev.ebus.core.IEBusController#setWatchdogTimerTimeout(int) + /** + * Sets the timeout duration for the watchdog timer. + * + * @param seconds the timeout duration in seconds + * @throws IllegalArgumentException if seconds is less than or equal to 0 */ @Override public void setWatchdogTimerTimeout(final int seconds) { - watchdogTimerTimeout = seconds; + if (seconds <= 0) { + throw new IllegalArgumentException("Watchdog timeout must be greater than 0 seconds"); + } + this.watchdogTimerTimeout = seconds; } + /** + * Called when the watchdog timer expires. + * Subclasses must implement this method to handle watchdog timeouts. + */ protected abstract void fireWatchDogTimer(); - protected void setConnectionStatus(final @NonNull ConnectionStatus status) { - - Objects.requireNonNull(status, "status"); + /** + * Validates that the thread pool is ready to execute tasks. + * + * @return true if the thread pool is initialized and not terminated + */ + protected boolean validateThreadPool() { + return threadPool != null && !threadPool.isTerminated(); + } + /** + * Updates the connection status and fires a status change event if needed. + * Thread-safe implementation that ensures events are only fired for actual status changes. + * + * @param status the new connection status to set + * @throws NullPointerException if status is null + */ + protected void setConnectionStatus(final @NonNull ConnectionStatus status) { + Objects.requireNonNull(status, "status cannot be null"); - // only run on a real status change + // Only fire event on actual status change if (this.connectionStatus != status) { this.connectionStatus = status; fireOnEBusConnectionStatusChange(status); @@ -342,6 +458,6 @@ protected void setConnectionStatus(final @NonNull ConnectionStatus status) { @Override public void run() { - throw new IllegalStateException("Method run() should be overwritten!"); + throw new IllegalStateException("Method run() must be overridden by subclasses"); } } diff --git a/src/main/java/de/csdev/ebus/core/EBusEbusdController.java b/src/main/java/de/csdev/ebus/core/EBusEbusdController.java index ae2c494..c2bec67 100644 --- a/src/main/java/de/csdev/ebus/core/EBusEbusdController.java +++ b/src/main/java/de/csdev/ebus/core/EBusEbusdController.java @@ -15,18 +15,17 @@ import java.io.OutputStreamWriter; import java.io.Writer; import java.net.Socket; -import java.net.UnknownHostException; import java.nio.ByteBuffer; import java.util.Arrays; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import de.csdev.ebus.core.EBusQueue.QueueEntry; import de.csdev.ebus.utils.CommonsUtils; import de.csdev.ebus.utils.EBusUtils; +import de.csdev.ebus.utils.StringUtil; +import de.csdev.ebus.utils.NumberUtils; /** * @author Christian Sowada - Initial contribution @@ -143,11 +142,11 @@ private ByteBuffer parseLine(String readLine) throws IOException, InterruptedExc logger.error("End of stream has been reached!"); reconnect(); - } else if (StringUtils.startsWith(readLine, "ERR:")) { + } else if (StringUtil.startsWith(readLine, "ERR:")) { logger.error(readLine); reconnect(); - } else if (StringUtils.equals(readLine, "direct mode started")) { + } else if (readLine != null && readLine.equals("direct mode started")) { logger.info("ebusd direct mode enabled!"); // start sender thread @@ -160,18 +159,24 @@ private ByteBuffer parseLine(String readLine) throws IOException, InterruptedExc } else if (readLine.startsWith("version:")) { try { String[] split = readLine.split(":"); - String value = StringUtils.trim(split[1]); + String value = split[1].trim(); - logger.info("Use ebusd version: {}", value); + if (logger.isInfoEnabled()) { + logger.info("Use ebusd version: {}", value.replaceAll("[\n\r]", "_")); + } - String version = StringUtils.trim(value.split(" ")[1]); + String version = value.split(" ")[1].trim(); String[] versionParts = version.split("\\."); if (NumberUtils.isDigits(versionParts[0]) && NumberUtils.isDigits(versionParts[1])) { - int versio = (NumberUtils.createInteger(versionParts[0]) * 1000) - + NumberUtils.createInteger(versionParts[1]); - if (versio < 3004) { - logger.warn("Your ebusd version is not supported. Please use a version >= 3.4 !"); + Integer major = NumberUtils.createInteger(versionParts[0]); + Integer minor = NumberUtils.createInteger(versionParts[1]); + if (major != null && minor != null) { + int versionNum = (major * 1000) + minor; + if (versionNum < 3004) { + logger.warn("Your ebusd version is not supported. Please use a version >= 3.4 !"); + } } + } } catch (Exception e) { // do not stop because of version check @@ -201,7 +206,7 @@ private ByteBuffer parseLine(String readLine) throws IOException, InterruptedExc b = convertEBusdDataToFullTelegram(EBusUtils.toByteArray2(split[0]), null); } else if (split[1].startsWith("ERR")) { - throw new EBusDataException(StringUtils.trim(split[2])); + throw new EBusDataException(split[2].trim()); } else { // // Master Slave diff --git a/src/main/java/de/csdev/ebus/core/EBusLowLevelController.java b/src/main/java/de/csdev/ebus/core/EBusLowLevelController.java index 76e31e8..238bd95 100644 --- a/src/main/java/de/csdev/ebus/core/EBusLowLevelController.java +++ b/src/main/java/de/csdev/ebus/core/EBusLowLevelController.java @@ -22,234 +22,311 @@ import de.csdev.ebus.utils.EBusUtils; /** - * @author Christian Sowada - Initial contribution + * Low-level implementation of an eBUS controller that handles direct + * communication + * with the physical eBUS interface. This controller manages: + * - Raw data transmission and reception + * - Connection management and auto-reconnection + * - Collision detection and handling + * - Telegram validation and processing + * - Synchronization and timing * + * @author Christian Sowada - Initial contribution */ public class EBusLowLevelController extends EBusControllerBase { private static final Logger logger = LoggerFactory.getLogger(EBusLowLevelController.class); + /** Maximum number of reconnection attempts before giving up */ + private static final int MAX_RECONNECT_ATTEMPTS = 10; + + /** Base delay in milliseconds between reconnection attempts */ + private static final long BASE_RECONNECT_DELAY = 5000L; + + /** Size of the receive buffer */ + private static final int RECEIVE_BUFFER_SIZE = 100; + + /** Default value for no round trip time measured */ + private static final long NO_ROUNDTRIP_TIME = -1L; + + /** The physical eBUS connection */ protected @NonNull IEBusConnection connection; - /** counts the re-connection tries */ + /** Counter for connection retry attempts */ private int reConnectCounter = 0; - private long sendRoundTrip = -1; + /** Last measured send-receive round trip time in nanoseconds */ + private long sendRoundTrip = NO_ROUNDTRIP_TIME; + /** + * Creates a new eBUS low-level controller with the specified connection. + * + * @param connection The physical eBUS connection to use + * @throws NullPointerException if connection is null + */ public EBusLowLevelController(@NonNull IEBusConnection connection) { super(); - - Objects.requireNonNull(connection, "connection"); - this.connection = connection; + this.connection = Objects.requireNonNull(connection, "connection cannot be null"); + logger.debug("Created new eBUS controller with connection type: {}", + connection.getClass().getSimpleName()); } + /** + * Returns the last measured send-receive round trip time in nanoseconds. + * + * @return The last measured round trip time, or -1 if no measurement is + * available + */ @Override public long getLastSendReceiveRoundtripTime() { return sendRoundTrip; } /** - * @return - * @throws EBusControllerException + * Gets the current eBUS connection instance. + * + * @return The current eBUS connection + * @throws EBusControllerException if the controller is not running */ public @NonNull IEBusConnection getConnection() throws EBusControllerException { if (!isRunning()) { - throw new EBusControllerException(); + throw new EBusControllerException("Cannot access connection - controller is not running"); } return connection; } /** - * Called event if a packet has been received + * Processes received eBUS data bytes. + * Handles data reception, state machine updates, and telegram processing. + * This method is called for each byte received from the eBUS. * - * @throws IOException + * @param data The received byte from the eBUS + * @throws IOException if an I/O error occurs during processing */ private void onEBusDataReceived(byte data) throws IOException { - if (!isRunning()) { - logger.trace("Skip event, thread was interrupted ..."); + logger.trace("Skipping data processing - controller interrupted"); return; } try { + // Update state machine with received byte machine.update(data); - } catch (EBusDataException e) { - this.fireOnEBusDataException(e, e.getSendId()); - } - if (machine.isWaitingForSlaveAnswer()) { - logger.trace("waiting for slave answer ..."); - } + if (machine.isWaitingForSlaveAnswer()) { + logger.trace("Awaiting slave response"); + } - // we received a SYN byte - if (machine.isSync()) { + // Process SYN byte reception + if (machine.isSync()) { + processSyncReceived(); + } + } catch (EBusDataException e) { + logger.debug("Data exception during processing: {}", e.getMessage()); + fireOnEBusDataException(e, e.getSendId()); + } + } - // try to send something if the send queue is not empty + /** + * Handles the reception of a SYN byte, which marks potential telegram + * boundaries. + * This includes sending queued data and processing complete telegrams. + * + * @throws IOException if an I/O error occurs during processing + */ + private void processSyncReceived() throws IOException { + try { + // Attempt to send queued data send(false); - // afterwards check for next sending slot - try { - queue.checkSendStatus(false); - } catch (EBusDataException e) { - fireOnEBusDataException(e, e.getSendId()); - } + // Check send queue status + queue.checkSendStatus(false); - // check if a complete and valid telegram is available + // Process complete telegram if available if (machine.isTelegramAvailable()) { - byte[] telegramData = machine.getTelegramData(); - // execute event + if (logger.isDebugEnabled()) { + logger.debug("Complete telegram received: {}", + EBusUtils.toHexDumpString(telegramData)); + } + fireOnEBusTelegramReceived(telegramData, null); machine.reset(); } + } catch (EBusDataException e) { + logger.debug("Data exception during sync processing: {}", e.getMessage()); + fireOnEBusDataException(e, e.getSendId()); } - } + /** + * Attempts to reconnect to the eBUS adapter using an exponential backoff + * strategy. + * After MAX_RECONNECT_ATTEMPTS failed attempts, the controller will be + * interrupted. + * + * @throws IOException if connection operations fail + * @throws InterruptedException if the thread is interrupted during reconnection + */ private void reconnect() throws IOException, InterruptedException { - if (!isRunning()) { - logger.trace("Skip reconnect, thread was interrupted ..."); + logger.trace("Skip reconnect, thread was interrupted"); return; } - logger.info("Try to reconnect to eBUS adapter ..."); - - // set connection status to connecting + logger.info("Attempting to reconnect to eBUS adapter"); setConnectionStatus(ConnectionStatus.CONNECTING); - if (reConnectCounter > 10) { + if (reConnectCounter > MAX_RECONNECT_ATTEMPTS) { + logger.error("Maximum reconnection attempts ({}) exceeded, shutting down controller", + MAX_RECONNECT_ATTEMPTS); reConnectCounter = -1; this.interrupt(); + return; + } - } else { - - reConnectCounter++; + reConnectCounter++; + long delayMillis = BASE_RECONNECT_DELAY * reConnectCounter; - logger.warn("Retry to connect to eBUS adapter in {} seconds ...", 5 * reConnectCounter); + logger.warn("Will retry connection in {} seconds (attempt {}/{})", + delayMillis / 1000, reConnectCounter, MAX_RECONNECT_ATTEMPTS); - Thread.sleep(5000L * reConnectCounter); + try { + Thread.sleep(delayMillis); + // Close existing connection before attempting to open a new one connection.close(); + if (connection.open()) { + logger.info("Successfully reconnected to eBUS adapter"); resetWatchdogTimer(); + } else { + logger.warn("Failed to open connection on attempt {}", reConnectCounter); } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw e; } - } /** - * Resend data if it's the first try or call resetSend() + * Attempts to resend the current telegram if no previous retry has been made. + * Implements a single-retry policy for failed transmissions. * - * @param secondTry - * @return - * @throws IOException + * @return true if the telegram will be retried, false if no retry is possible */ private boolean resend() { - QueueEntry entry = queue.getCurrent(); - if (isRunning() && entry != null && !entry.secondTry) { - entry.secondTry = true; - return true; + if (!isRunning() || entry == null) { + logger.debug("Cannot resend - controller not running or no current entry"); + return false; + } - } else { - logger.warn("Resend failed, remove data from sending queue ..."); + if (entry.secondTry) { + logger.warn("Maximum retry attempts reached for telegram ID {}, removing from queue", + entry.id); queue.resetSendQueue(); return false; } + + entry.secondTry = true; + logger.debug("Scheduling retry for telegram ID {}", entry.id); + return true; } + /** + * Main controller loop that handles the eBUS communication. + * Manages connection state, data reception, and error recovery. + */ @Override @SuppressWarnings("java:S3776") public void run() { + logger.info("Starting eBUS low level controller"); + // Initialize thread pools for event handling initThreadPool(); - int read = -1; - - byte[] buffer = new byte[100]; + int bytesRead = -1; + byte[] receiveBuffer = new byte[RECEIVE_BUFFER_SIZE]; + // Initialize connection if needed try { if (!connection.isOpen()) { - + logger.debug("Opening initial connection to eBUS"); setConnectionStatus(ConnectionStatus.CONNECTING); - connection.open(); } } catch (IOException e) { - logger.error(EBusConsts.LOG_ERR_DEF, e); + logger.error("Failed to establish initial connection: {}", e.getMessage(), e); fireOnConnectionException(e); } resetWatchdogTimer(); - // loop until interrupt or reconnector count is -1 (to many retries) - while (!(Thread.interrupted() || reConnectCounter == -1)) { + // Main control loop - continue until interrupted or max reconnection attempts + // exceeded + while (!Thread.interrupted() && reConnectCounter != -1) { try { - if (!connection.isOpen()) { reconnect(); - } else { - - // the connection is now connected + // Connection is established setConnectionStatus(ConnectionStatus.CONNECTED); - // read byte from connector - read = connection.readBytes(buffer); - - if (read == -1) { - logger.debug("eBUS read timeout occured, no data on bus ..."); - throw new IOException("End of eBUS stream reached!"); - - } else { - for (int i = 0; i < read; i++) { - onEBusDataReceived(buffer[i]); + // Read data from the bus + bytesRead = connection.readBytes(receiveBuffer); - } + if (bytesRead == -1) { + logger.debug("eBUS read timeout occurred, no data on bus"); + throw new IOException("End of eBUS stream reached"); + } - // reset with received data - resetWatchdogTimer(); - reConnectCounter = 0; + // Process received bytes + for (int i = 0; i < bytesRead; i++) { + onEBusDataReceived(receiveBuffer[i]); } + + // Reset watchdog and connection counter on successful read + resetWatchdogTimer(); + reConnectCounter = 0; } } catch (InterruptedIOException | InterruptedException e) { - // bubble interrrupt flag to outer main loop + logger.debug("Controller interrupted, stopping main loop"); Thread.currentThread().interrupt(); } catch (IOException e) { - logger.error("An IO exception has occured! Try to reconnect eBUS connector ...", e); + logger.error("IO exception occurred - attempting to reconnect: {}", e.getMessage(), e); fireOnConnectionException(e); try { reconnect(); - } catch (IOException e1) { - logger.error(e.toString(), e1); - } catch (InterruptedException e1) { - // bubble interrrupt flag to outer main loop + } catch (IOException reconnectError) { + logger.error("Failed to reconnect: {}", reconnectError.getMessage(), reconnectError); + } catch (InterruptedException interrupted) { + logger.debug("Reconnection attempt interrupted"); Thread.currentThread().interrupt(); } } catch (BufferOverflowException e) { - logger.error( - "eBUS telegram buffer overflow - not enough sync bytes received! Try to adjust eBUS adapter."); - machine.reset(); + logger.error("eBUS telegram buffer overflow - insufficient sync bytes received. " + + "Consider adjusting eBUS adapter settings."); + // store nano time to measure send receive roundtrip time } catch (Exception e) { - logger.error(e.toString(), e); + logger.error("Unexpected error in main loop: {}", e.getMessage(), e); machine.reset(); } - } // while loop + } - // interrupted flag is not active anymore + logger.info("eBUS controller main loop terminated, performing cleanup"); try { dispose(); } catch (InterruptedException e) { - logger.error("error!", e); + logger.error("Interrupted during controller cleanup", e); Thread.currentThread().interrupt(); } } @@ -260,7 +337,6 @@ public void run() { * @param secondTry * @throws IOException */ - @SuppressWarnings("java:S3776") private void send(boolean secondTry) throws IOException { if (!isRunning()) { @@ -407,7 +483,6 @@ else if ((byte) (readByte & 0x0F) == (byte) (b & 0x0F)) { if (sendMachine.isWaitingForSlaveAnswer()) { logger.trace("Waiting for slave answer ..."); - // read input data until the telegram is complete or fails while (!sendMachine.isWaitingForMasterACK() && !sendMachine.isWaitingForMasterSYN()) { read = connection.readByte(true); if (read != -1) { @@ -457,39 +532,51 @@ else if ((byte) (readByte & 0x0F) == (byte) (b & 0x0F)) { } } + /** + * Performs a clean shutdown of the controller. + * Closes connections, updates status, and releases resources. + * + * @throws InterruptedException if interrupted during cleanup + */ @Override protected void dispose() throws InterruptedException { + logger.info("Shutting down eBUS controller"); - logger.info("eBUS connection thread is shuting down ..."); - - // set connection status to disconnected + // Update status first to prevent new operations setConnectionStatus(ConnectionStatus.DISCONNECTED); + // Clean up base class resources super.dispose(); - // ******************************* - // ** end of thread ** - // ******************************* - - // disconnect the connector e.g. close serial port - try { - if (connection != null) { + // Close physical connection + if (connection != null) { + logger.debug("Closing eBUS connection"); + try { connection.close(); + } catch (IOException e) { + logger.error("Error closing eBUS connection: {}", e.getMessage(), e); } - } catch (IOException e) { - logger.error(e.toString(), e); } + logger.info("eBUS controller shutdown complete"); } + /** + * Handles watchdog timer expiration. + * Forces a connection close to trigger reconnection on timeout. + */ @Override protected void fireWatchDogTimer() { - logger.warn("eBUS Watchdog Timer!"); + logger.warn("Watchdog timer expired - forcing connection reset"); try { + logger.debug("Closing connection due to watchdog timeout"); connection.close(); } catch (IOException e) { - logger.error(EBusConsts.LOG_ERR_DEF, e); + logger.error("Error closing connection on watchdog timeout: {}", e.getMessage(), e); } + + // Reset send round trip time measurement + sendRoundTrip = NO_ROUNDTRIP_TIME; } } diff --git a/src/main/java/de/csdev/ebus/core/EBusReceiveStateMachine.java b/src/main/java/de/csdev/ebus/core/EBusReceiveStateMachine.java index b22879d..8bc7dff 100644 --- a/src/main/java/de/csdev/ebus/core/EBusReceiveStateMachine.java +++ b/src/main/java/de/csdev/ebus/core/EBusReceiveStateMachine.java @@ -260,7 +260,7 @@ public void update(final byte dataByte) throws EBusDataException { // add data to result and crc bb.put(data); - crc = EBusUtils.crc8_tab(data, (byte) 0); + crc = EBusUtils.crc8Tab(data, (byte) 0); setState(State.SRC_ADDR); @@ -285,7 +285,7 @@ public void update(final byte dataByte) throws EBusDataException { // add data to result and crc bb.put(data); - crc = EBusUtils.crc8_tab(data, crc); + crc = EBusUtils.crc8Tab(data, crc); setState(State.TGT_ADDR); break; @@ -298,7 +298,7 @@ public void update(final byte dataByte) throws EBusDataException { // add data to result and crc bb.put(data); - crc = EBusUtils.crc8_tab(data, crc); + crc = EBusUtils.crc8Tab(data, crc); setState(State.PRIMARY_CMD); break; @@ -311,7 +311,7 @@ public void update(final byte dataByte) throws EBusDataException { // add data to result and crc bb.put(data); - crc = EBusUtils.crc8_tab(data, crc); + crc = EBusUtils.crc8Tab(data, crc); setState(State.SECONDARY_CMD); break; @@ -335,7 +335,7 @@ public void update(final byte dataByte) throws EBusDataException { // add data to result and crc bb.put(data); - crc = EBusUtils.crc8_tab(data, crc); + crc = EBusUtils.crc8Tab(data, crc); setState(len == 0 ? State.DATA1 : State.LENGTH1); break; @@ -347,7 +347,7 @@ public void update(final byte dataByte) throws EBusDataException { throwExceptionIfSYN(data); // add data to crc - crc = EBusUtils.crc8_tab(data, crc); + crc = EBusUtils.crc8Tab(data, crc); if (data == EBusConsts.ESCAPE) { isEscapedByte = true; @@ -473,7 +473,7 @@ public void update(final byte dataByte) throws EBusDataException { // add data to result and crc bb.put(data); - crc = EBusUtils.crc8_tab(data, (byte) 0); + crc = EBusUtils.crc8Tab(data, (byte) 0); if (data > 16) { throw new EBusDataException("Slave Data Length too large!", @@ -497,7 +497,7 @@ public void update(final byte dataByte) throws EBusDataException { throwExceptionIfSYN(data); // add symbol to crc check - crc = EBusUtils.crc8_tab(data, crc); + crc = EBusUtils.crc8Tab(data, crc); // is symbol an escape symbol, then we have to decode the next symbol if (data == EBusConsts.ESCAPE) { diff --git a/src/main/java/de/csdev/ebus/utils/ArrayUtil.java b/src/main/java/de/csdev/ebus/utils/ArrayUtil.java new file mode 100644 index 0000000..21b49c9 --- /dev/null +++ b/src/main/java/de/csdev/ebus/utils/ArrayUtil.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2017-2025 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package de.csdev.ebus.utils; + +import java.util.Arrays; +import java.util.Objects; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Utility class to replace Apache Commons Lang ArrayUtils + * @author Christian Sowada - Initial contribution + */ +public final class ArrayUtil { + private static final String ARRAY_MUST_NOT_BE_NULL = "array must not be null"; + + private ArrayUtil() { + // Utility class + } + + /** + * Checks if an array is empty or null. + */ + public static boolean isEmpty(byte @Nullable [] array) { + return array == null || array.length == 0; + } + + /** + * Checks if an array is empty or null. + */ + public static boolean isEmpty(String @Nullable [] array) { + return array == null || array.length == 0; + } + + /** + * Checks if an array is not empty and not null. + */ + public static boolean isNotEmpty(byte @Nullable [] array) { + return !isEmpty(array); + } + + /** + * Copies the specified array, truncating or padding with zeros (if necessary) + * so the copy has the specified length. + */ + public static byte[] copyOf(byte @Nullable [] original, int newLength) { + Objects.requireNonNull(original, "original must not be null"); + return Arrays.copyOf(original, newLength); + } + + /** + * Reverses the order of the given array. + */ + public static void reverse(byte @Nullable [] array) { + Objects.requireNonNull(array, ARRAY_MUST_NOT_BE_NULL); + int i = 0; + int j = array.length - 1; + byte tmp; + while (j > i) { + tmp = array[j]; + array[j] = array[i]; + array[i] = tmp; + j--; + i++; + } + } + + /** + * Creates a new array containing the specified elements. + */ + public static byte[] toPrimitive(@NonNull Byte[] array) { + Objects.requireNonNull(array, ARRAY_MUST_NOT_BE_NULL); + final byte[] result = new byte[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i] != null ? array[i] : 0; + } + return result; + } + + /** + * Converts an array of primitive bytes to objects. + */ + public static @NonNull Byte[] toObject(byte @Nullable [] array) { + Objects.requireNonNull(array, ARRAY_MUST_NOT_BE_NULL); + final Byte[] result = new Byte[array.length]; + for (int i = 0; i < array.length; i++) { + result[i] = array[i]; + } + return result; + } + + /** + * Creates a copy of an array, or returns null if the array is null. + */ + public static byte @Nullable [] clone(byte @Nullable [] array) { + return array == null ? null : Arrays.copyOf(array, array.length); + } + + /** + * Checks if the given array contains the specified value. + * + * @param array the array to search through (may be null) + * @param valueToFind the value to find + * @return true if the array contains the specified value + */ + public static boolean contains(String @Nullable [] array, String valueToFind) { + if (array == null || isEmpty(array)) { + return false; + } + + for (String element : array) { + if (element.equals(valueToFind)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/de/csdev/ebus/utils/CollectionUtils.java b/src/main/java/de/csdev/ebus/utils/CollectionUtils.java index 6c0c30e..711865b 100644 --- a/src/main/java/de/csdev/ebus/utils/CollectionUtils.java +++ b/src/main/java/de/csdev/ebus/utils/CollectionUtils.java @@ -27,7 +27,7 @@ private CollectionUtils() { throw new IllegalStateException("Utility class"); } - public static final @NonNull List<@NonNull T> emptyList() { + public static final @NonNull List emptyList() { return Objects.requireNonNull(Collections.emptyList()); } diff --git a/src/main/java/de/csdev/ebus/utils/EBusConsoleUtils.java b/src/main/java/de/csdev/ebus/utils/EBusConsoleUtils.java index 33b3b7e..63a8d6b 100644 --- a/src/main/java/de/csdev/ebus/utils/EBusConsoleUtils.java +++ b/src/main/java/de/csdev/ebus/utils/EBusConsoleUtils.java @@ -17,7 +17,6 @@ import java.util.Map.Entry; import java.util.Objects; -import org.apache.commons.lang3.StringUtils; import org.eclipse.jdt.annotation.NonNull; import org.eclipse.jdt.annotation.Nullable; import org.slf4j.Logger; @@ -66,12 +65,12 @@ public static String bruteforceData(byte @Nullable [] data) throws EBusTypeExcep EBusTypeRegistry typeRegistry = new EBusTypeRegistry(); - IEBusType typeD1C = typeRegistry.getType(EBusTypeData1c.TYPE_DATA1C); - IEBusType typeBCD = typeRegistry.getType(EBusTypeBCD.TYPE_BCD); - IEBusType typeWord = typeRegistry.getType(EBusTypeWord.TYPE_WORD); - IEBusType typeInt = typeRegistry.getType(EBusTypeInteger.TYPE_INTEGER); - IEBusType typeD2B = typeRegistry.getType(EBusTypeData2b.TYPE_DATA2B); - IEBusType typeD2C = typeRegistry.getType(EBusTypeData2c.TYPE_DATA2C); + @NonNull IEBusType typeD1C = Objects.requireNonNull(typeRegistry.getType(EBusTypeData1c.TYPE_DATA1C)); + @NonNull IEBusType typeBCD = Objects.requireNonNull(typeRegistry.getType(EBusTypeBCD.TYPE_BCD)); + @NonNull IEBusType typeWord = Objects.requireNonNull(typeRegistry.getType(EBusTypeWord.TYPE_WORD)); + @NonNull IEBusType typeInt = Objects.requireNonNull(typeRegistry.getType(EBusTypeInteger.TYPE_INTEGER)); + @NonNull IEBusType typeD2B = Objects.requireNonNull(typeRegistry.getType(EBusTypeData2b.TYPE_DATA2B)); + @NonNull IEBusType typeD2C = Objects.requireNonNull(typeRegistry.getType(EBusTypeData2c.TYPE_DATA2C)); String format = String.format("%-4s%-13s%-13s%-13s%-13s%-13s%-13s%-13s", "Pos", "WORD", "Int", "UInt8", "DATA2B", "DATA2C", "DATA1c", "BCD"); @@ -154,9 +153,9 @@ public static String getDeviceTableInformation(@NonNull Collection<@NonNull IEBu "Identifier", "Device", "Manufacture", "ID", "Firmware", "Hardware", "Last Activity")); sb.append(String.format("%-2s-+-%-2s-+-%-14s-+-%-14s-+-%-20s-+-%-2s-+-%-10s-+-%-10s-+-%-20s%n", - StringUtils.repeat("-", 2), StringUtils.repeat("-", 2), StringUtils.repeat("-", 14), - StringUtils.repeat("-", 14), StringUtils.repeat("-", 20), StringUtils.repeat("-", 2), - StringUtils.repeat("-", 10), StringUtils.repeat("-", 10), StringUtils.repeat("-", 20))); + StringUtil.repeat("-", 2), StringUtil.repeat("-", 2), StringUtil.repeat("-", 14), + StringUtil.repeat("-", 14), StringUtil.repeat("-", 20), StringUtil.repeat("-", 2), + StringUtil.repeat("-", 10), StringUtil.repeat("-", 10), StringUtil.repeat("-", 20))); for (EBusDevice device : deviceTable.getDeviceTable()) { @@ -176,7 +175,7 @@ public static String getDeviceTableInformation(@NonNull Collection<@NonNull IEBu } - sb.append(StringUtils.repeat("-", 118) + "\n"); + sb.append(StringUtil.repeat("-", 118) + "\n"); sb.append("MA = Master Address / SA = Slave Address / ID = Manufacture ID\n"); return sb.toString(); @@ -209,14 +208,14 @@ public static String getDeviceTableInformation(@NonNull Collection<@NonNull IEBu int len = msg.length(); sb.append("\n"); - sb.append(StringUtils.repeat("*", len) + "\n"); + sb.append(StringUtil.repeat("*", len) + "\n"); sb.append(msg + "\n"); msg = "** !!! Warning: All following results are wrong and only displayed for information purpose !!!"; - msg += StringUtils.repeat(" ", len - msg.length() - 2) + "**"; + msg += StringUtil.repeat(" ", len - msg.length() - 2) + "**"; sb.append(msg + "\n"); - sb.append(StringUtils.repeat("*", len) + "\n"); + sb.append(StringUtil.repeat("*", len) + "\n"); sb.append("\n"); return Objects.requireNonNull(sb.toString()); @@ -374,16 +373,16 @@ public static String getDeviceTableInformation(@NonNull Collection<@NonNull IEBu private static @NonNull String createTelegramResoverRow(int pos, int length, int textStart, String text) { StringBuilder sb = new StringBuilder(); - String repeat = StringUtils.repeat("^^ ", length); + String repeat = StringUtil.repeat("^^ ", length); if (repeat.length() > 0) { repeat = repeat.substring(0, repeat.length() - 1); } - sb.append(StringUtils.repeat(" ", pos * 3)); + sb.append(StringUtil.repeat(" ", pos * 3)); sb.append(repeat); - sb.append(StringUtils.repeat("-", textStart - sb.length())); + sb.append(StringUtil.repeat("-", textStart - sb.length())); sb.append(" "); sb.append(text); sb.append("\n"); diff --git a/src/main/java/de/csdev/ebus/utils/EBusDateTime.java b/src/main/java/de/csdev/ebus/utils/EBusDateTime.java index a7c584a..7451b3a 100644 --- a/src/main/java/de/csdev/ebus/utils/EBusDateTime.java +++ b/src/main/java/de/csdev/ebus/utils/EBusDateTime.java @@ -70,9 +70,6 @@ public boolean isAnyTime() { public String toString() { SimpleDateFormat format = null; - if (calendar == null) { - return ""; - } if (anyDate && anyTime) { return ""; @@ -117,11 +114,7 @@ public boolean equals(@Nullable Object obj) { if (anyTime != other.anyTime) { return false; } - if (calendar == null) { - if (other.calendar != null) { - return false; - } - } else if (!calendar.equals(other.calendar)) { + if (!calendar.equals(other.calendar)) { return false; } return true; diff --git a/src/main/java/de/csdev/ebus/utils/EBusUtils.java b/src/main/java/de/csdev/ebus/utils/EBusUtils.java index 26fa373..a95dd30 100644 --- a/src/main/java/de/csdev/ebus/utils/EBusUtils.java +++ b/src/main/java/de/csdev/ebus/utils/EBusUtils.java @@ -11,7 +11,7 @@ import java.nio.ByteBuffer; import java.util.Objects; -import org.apache.commons.lang3.StringUtils; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -135,7 +135,7 @@ public static byte crc8(final byte @Nullable [] data, final int len) { for (int i = 0; i < len; i++) { byte b = data[i]; - ucCrc = crc8_tab(b, ucCrc); + ucCrc = crc8Tab(b, ucCrc); } return ucCrc; } @@ -147,7 +147,7 @@ public static byte crc8(final byte @Nullable [] data, final int len) { * @param crcInit The current crc result or another start value * @return The crc result */ - public static byte crc8_tab(final byte data, final byte crcInit) { + public static byte crc8Tab(final byte data, final byte crcInit) { short ci = (short) (crcInit & 0xFF); return (byte) (CRC_TAB_8_VALUE[ci] ^ (data & 0xFF)); } @@ -241,10 +241,14 @@ public static boolean isValidAddress(final byte address) { * @return */ public static @Nullable Byte toByte(final @Nullable String hexDumpString) { - if (StringUtils.isEmpty(hexDumpString)) { + if (StringUtil.isEmpty(hexDumpString)) { + return null; + } + byte[] arr = toByteArray(hexDumpString); + if (arr.length == 0) { return null; } - return toByteArray(hexDumpString)[0]; + return arr[0]; } /** @@ -282,7 +286,7 @@ public static byte[] toByteArray(@Nullable ByteBuffer buffer) { * @return */ public static byte[] toByteArray(final @Nullable String hexDumpString) throws NumberFormatException { - if (hexDumpString == null || StringUtils.isEmpty(hexDumpString)) { + if (hexDumpString == null || StringUtil.isEmpty(hexDumpString)) { return new byte[0]; } @@ -307,7 +311,7 @@ public static byte[] toByteArray2(final @Nullable String hexDumpString) throws N String h = hexDumpString; - if (h == null || StringUtils.isEmpty(h)) { + if (h == null || StringUtil.isEmpty(h)) { return new byte[0]; } @@ -334,7 +338,7 @@ public static String mergeHexDumpStrings(final @Nullable String... args) { StringBuilder sb = new StringBuilder(); for (String string : args) { - if (string != null && StringUtils.isNotEmpty(string)) { + if (string != null && StringUtil.isNotEmpty(string)) { sb.append(string.length() % 2 == 0 ? string : "0" + string); } } diff --git a/src/main/java/de/csdev/ebus/utils/FieldUtil.java b/src/main/java/de/csdev/ebus/utils/FieldUtil.java new file mode 100644 index 0000000..4fc429e --- /dev/null +++ b/src/main/java/de/csdev/ebus/utils/FieldUtil.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2017-2025 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package de.csdev.ebus.utils; + +import java.lang.reflect.Field; + +import org.eclipse.jdt.annotation.Nullable; + +/** + * Utility class to replace Apache Commons Lang FieldUtils + * + * @author Christian Sowada - Initial contribution + */ +public final class FieldUtil { + private FieldUtil() { + // Utility class + } + + /** + * Gets an accessible Field by name, breaking scope if requested. + */ + public static @Nullable Field getField(final Class cls, String fieldName, boolean forceAccess) { + if (cls == null) { + throw new IllegalArgumentException("The class must not be null"); + } + if (fieldName == null) { + throw new IllegalArgumentException("The field name must not be null"); + } + + // check up the superclass hierarchy + for (Class acls = cls; acls != null; acls = acls.getSuperclass()) { + try { + final Field field = acls.getDeclaredField(fieldName); + if (!field.isAccessible()) { + if (forceAccess) { + field.setAccessible(true); + } else { + return null; + } + } + return field; + } catch (final NoSuchFieldException ignored) { + // ignore + } + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/de/csdev/ebus/utils/NumberUtils.java b/src/main/java/de/csdev/ebus/utils/NumberUtils.java index bb08357..655589a 100644 --- a/src/main/java/de/csdev/ebus/utils/NumberUtils.java +++ b/src/main/java/de/csdev/ebus/utils/NumberUtils.java @@ -88,4 +88,32 @@ private NumberUtils() { return (byte) (high * 10 + low); } + /** + * Checks if a String contains only digits. + */ + public static boolean isDigits(@Nullable String str) { + if (str == null || str.isEmpty()) { + return false; + } + for (int i = 0; i < str.length(); i++) { + if (!Character.isDigit(str.charAt(i))) { + return false; + } + } + return true; + } + + /** + * Converts a String to an Integer, handling nulls. + */ + public static @Nullable Integer createInteger(@Nullable String str) { + if (str == null) { + return null; + } + try { + return Integer.valueOf(str); + } catch (NumberFormatException e) { + return null; + } + } } diff --git a/src/main/java/de/csdev/ebus/utils/ObjectUtil.java b/src/main/java/de/csdev/ebus/utils/ObjectUtil.java new file mode 100644 index 0000000..8d0ad8b --- /dev/null +++ b/src/main/java/de/csdev/ebus/utils/ObjectUtil.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2017-2025 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package de.csdev.ebus.utils; + +import java.util.Objects; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Utility class to replace Apache Commons Lang ObjectUtils + * + * @author Christian Sowada - Initial contribution + */ +public final class ObjectUtil { + private ObjectUtil() { + // Utility class + } + + /** + * Returns the first non-null value in the array + */ + @SafeVarargs + public static @Nullable T firstNonNull(@Nullable T... values) { + if (values == null) { + return null; + } + for (@Nullable T val : values) { + if (val != null) { + return val; + } + } + return null; + } + + /** + * Compares two objects for equality, handling nulls + */ + public static boolean equals(@Nullable Object a, @Nullable Object b) { + return Objects.equals(a, b); + } + + /** + * Returns a default value if the object passed is null + */ + public static @NonNull T defaultIfNull(@Nullable T object, @NonNull T defaultValue) { + Objects.requireNonNull(defaultValue, "defaultValue must not be null"); + return object != null ? object : defaultValue; + } + + /** + * Returns a hash code for an object, handling null + */ + public static int hashCode(@Nullable Object obj) { + return obj != null ? obj.hashCode() : 0; + } + + /** + * Gets a toString for the object, handling null + */ + public static @NonNull String toString(@Nullable Object obj) { + return obj != null ? obj.toString() : "null"; + } +} \ No newline at end of file diff --git a/src/main/java/de/csdev/ebus/utils/StringUtil.java b/src/main/java/de/csdev/ebus/utils/StringUtil.java new file mode 100644 index 0000000..2035963 --- /dev/null +++ b/src/main/java/de/csdev/ebus/utils/StringUtil.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2017-2025 by the respective copyright holders. + * + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package de.csdev.ebus.utils; + +import java.util.Objects; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; + +/** + * Utility class to replace Apache Commons Lang StringUtils + * + * @author Christian Sowada - Initial contribution + */ +public final class StringUtil { + private StringUtil() { + // Utility class + } + + /** + * Checks if a String is empty ("") or null. + */ + public static boolean isEmpty(@Nullable String str) { + return str == null || str.isEmpty(); + } + + /** + * Checks if a String is not empty ("") and not null. + */ + public static boolean isNotEmpty(@Nullable String str) { + return !isEmpty(str); + } + + /** + * Returns either the passed in String, or if the String is empty or null, + * the value supplied by defaultStr. + */ + public static @NonNull String defaultIfEmpty(@Nullable String str, @NonNull String defaultStr) { + Objects.requireNonNull(defaultStr, "defaultStr must not be null"); + return isEmpty(str) ? defaultStr : Objects.requireNonNull(str); + } + + /** + * Checks if a CharSequence contains a search CharSequence irrespective of case. + */ + public static boolean containsIgnoreCase(@Nullable String str, @Nullable String searchStr) { + if (str == null || searchStr == null) { + return false; + } + return str.toLowerCase().contains(searchStr.toLowerCase()); + } + + /** + * Check if a String starts with a specified prefix (case-sensitive). + */ + public static boolean startsWith(@Nullable String str, @Nullable String prefix) { + if (str == null || prefix == null) { + return false; + } + return str.startsWith(prefix); + } + + /** + * Repeat a String the specified number of times to form a new String. + */ + public static @NonNull String repeat(@NonNull String str, int repeat) { + Objects.requireNonNull(str, "str must not be null"); + if (repeat <= 0) { + return ""; + } + if (repeat == 1) { + return str; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < repeat; i++) { + sb.append(str); + } + return sb.toString(); + } +} \ No newline at end of file