LocationGroup.java
/*
* Copyright 2005-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openwms.common.location;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.FetchType;
import jakarta.persistence.ForeignKey;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import org.ameba.exception.ServiceLayerException;
import org.openwms.common.StateChangeException;
import org.openwms.common.account.Account;
import org.openwms.common.location.api.LocationGroupMode;
import org.openwms.common.location.api.LocationGroupState;
import org.springframework.util.Assert;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Set;
/**
* A LocationGroup is a logical group of {@code Location}s with same characteristics.
*
* @author Heiko Scherrer
* @GlossaryTerm
* @see org.openwms.common.location.Location
*/
@Entity
@Table(name = "COM_LOCATION_GROUP", uniqueConstraints =
@UniqueConstraint(name = "UC_LG_NAME", columnNames = "C_NAME")
)
public class LocationGroup extends Target implements Serializable {
/** Unique identifier of a {@code LocationGroup}. */
@Column(name = "C_NAME", nullable = false, length = LENGTH_NAME)
@NotBlank
@Size(min = 1, max = LENGTH_NAME)
private String name;
/** Length of the name field; used for telegram mapping and for column definition. */
public static final int LENGTH_NAME = 255;
/** The LocationGroup might be assigned to an {@link Account}. */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "C_ACCOUNT", referencedColumnName = "C_IDENTIFIER", foreignKey = @ForeignKey(name = "FK_LG_ACC"))
private Account account;
/** Description of the {@code LocationGroup}. */
@Column(name = "C_DESCRIPTION")
private String description;
/** A type can be assigned to a {@code LocationGroup}. */
@Column(name = "C_GROUP_TYPE")
private String groupType;
/** Is the {@code LocationGroup} included in the calculation of {@code TransportUnit}s. */
@Column(name = "C_GROUP_COUNTING_ACTIVE")
private boolean locationGroupCountingActive = true;
/** The operation mode is controlled by the subsystem and defines the physical mode a {@code LocationGroup} is currently able to operate in. */
@Column(name = "C_OP_MODE")
@NotBlank
private String operationMode = LocationGroupMode.INFEED_AND_OUTFEED;
/** State of infeed, controlled by the subsystem only. */
@Column(name = "C_GROUP_STATE_IN")
@Enumerated(EnumType.STRING)
@NotNull
private LocationGroupState groupStateIn = LocationGroupState.AVAILABLE;
/** References the {@code LocationGroup} that locked this {@code LocationGroup} for infeed. */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "C_IN_LOCKER", foreignKey = @ForeignKey(name = "FK_LG_LG_INLOCKER"))
private LocationGroup stateInLocker;
/** State of outfeed. */
@Column(name = "C_GROUP_STATE_OUT")
@Enumerated(EnumType.STRING)
@NotNull
private LocationGroupState groupStateOut = LocationGroupState.AVAILABLE;
/** References the {@code LocationGroup} that locked this {@code LocationGroup} for outfeed. */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "C_OUT_LOCKER", foreignKey = @ForeignKey(name = "FK_LG_LG_OUTLOCKER"))
private LocationGroup stateOutLocker;
/** Maximum fill level of the {@code LocationGroup}. */
@Column(name = "C_MAX_FILL_LEVEL")
private float maxFillLevel = 0;
/** The subsystem like a PLC, that manages this {@code LocationGroup}. */
@Embedded
private Subsystem subsystem;
/* ------------------- collection mapping ------------------- */
/** Parent {@code LocationGroup}. */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "C_PARENT", foreignKey = @ForeignKey(name = "FK_LG_LG_PARENT"))
private LocationGroup parent;
/** Child {@code LocationGroup}s. */
@OneToMany(mappedBy = "parent", cascade = {CascadeType.ALL})
private Set<LocationGroup> locationGroups = new HashSet<>();
/** Child {@link Location}s. */
@OneToMany(mappedBy = "locationGroup")
private Set<Location> locations = new HashSet<>();
/*~ ----------------------------- constructors ------------------- */
/** Dear JPA... */
protected LocationGroup() { }
/**
* Create a new {@code LocationGroup} with a unique name.
*
* @param name The name of the {@code LocationGroup} must not be {@literal null}
*/
public LocationGroup(@NotBlank String name) {
Assert.hasText(name, "Creation of LocationGroup with name null");
this.name = name;
}
/*~ ----------------------------- methods ------------------- */
/**
* Returns the name of the {@code LocationGroup}.
*
* @return The name of the {@code LocationGroup}
*/
public String getName() {
return name;
}
/**
* Return the {@link Account} this {@code LocationGroup} is assigned to.
*
* @return The {@link Account}
*/
public Account getAccount() {
return account;
}
/**
* Sets the {@link Account} for this {@code LocationGroup}.
*
* @param account The {@link Account} to set for this {@code LocationGroup}
*/
public void setAccount(Account account) {
this.account = account;
}
/**
* Check whether infeed is allowed for the {@code LocationGroup}.
*
* @return {@literal true} if allowed, otherwise {@literal false}.
*/
public boolean isInfeedAllowed() {
return (getGroupStateIn() == LocationGroupState.AVAILABLE);
}
/**
* Check whether infeed of the {@code LocationGroup} is blocked.
*
* @return {@literal true} if blocked, otherwise {@literal false}.
*/
public boolean isInfeedBlocked() {
return !isInfeedAllowed();
}
/**
* Check whether outfeed is allowed for the {@code LocationGroup}.
*
* @return {@literal true} if allowed, otherwise {@literal false}.
*/
public boolean isOutfeedAllowed() {
return (getGroupStateOut() == LocationGroupState.AVAILABLE);
}
/**
* Check whether outfeed of the {@code LocationGroup} is blocked.
*
* @return {@literal true} if blocked, otherwise {@literal false}.
*/
public boolean isOutfeedBlocked() {
return !isOutfeedAllowed();
}
/**
* Get the current operation mode this {@code LocationGroup} operates in.
*
* @return The operational mode
*/
public String getOperationMode() {
return operationMode;
}
/**
* Set the current operation mode this {@code LocationGroup} can operate in.
*
* @param operationMode The mode as an extensible String
* @see LocationGroupMode
*/
public void setOperationMode(@NotBlank String operationMode) {
this.operationMode = operationMode;
this.locationGroups.forEach(lg -> lg.setOperationMode(operationMode));
}
/**
* Returns the infeed state of the {@code LocationGroup}.
*
* @return The state of infeed
*/
public LocationGroupState getGroupStateIn() {
return this.groupStateIn;
}
/**
* Change the infeed state of the {@code LocationGroup}.
*
* @param newGroupStateIn The state to set
*/
public void changeGroupStateIn(LocationGroupState newGroupStateIn) {
if (stateInLocker != null && stateInLocker != this) {
throw new StateChangeException("The LocationGroup's state is blocked by any other LocationGroup and cannot be changed");
}
groupStateIn = newGroupStateIn;
locationGroups.forEach(lg -> lg.changeGroupStateIn(newGroupStateIn, this));
}
/**
* Change the infeed state of the {@code LocationGroup}.
*
* @param newGroupStateIn The state to set
* @param lockLG The {@code LocationGroup} that wants to lock/unlock this {@code LocationGroup}.
*/
private void changeGroupStateIn(LocationGroupState newGroupStateIn, LocationGroup lockLG) {
if (groupStateIn == LocationGroupState.NOT_AVAILABLE && newGroupStateIn == LocationGroupState.AVAILABLE) {
// unlock
stateInLocker = null;
}
if (groupStateIn == LocationGroupState.AVAILABLE && newGroupStateIn == LocationGroupState.NOT_AVAILABLE) {
// lock
stateInLocker = lockLG;
}
groupStateIn = newGroupStateIn;
locationGroups.forEach(lg -> lg.changeGroupStateIn(newGroupStateIn, lockLG));
}
/**
* Return the outfeed state of the {@code LocationGroup}.
*
* @return The state of outfeed
*/
public LocationGroupState getGroupStateOut() {
return groupStateOut;
}
/**
* Change the outfeed state of the {@code LocationGroup}.
*
* @param newGroupStateOut The state to set
*/
public void changeGroupStateOut(LocationGroupState newGroupStateOut) {
if (stateOutLocker != null && stateOutLocker != this) {
throw new StateChangeException("The LocationGroup's state is blocked by any other LocationGroup and cannot be changed");
}
groupStateOut = newGroupStateOut;
locationGroups.forEach(lg -> lg.changeGroupStateOut(newGroupStateOut, this));
}
/**
* Set the outfeed state of the {@code LocationGroup}.
*
* @param gStateOut The state to set
* @param lockLg The {@code LocationGroup} that wants to lock/unlock this {@code LocationGroup}.
*/
void changeGroupStateOut(LocationGroupState gStateOut, LocationGroup lockLg) {
if (this.groupStateOut == LocationGroupState.NOT_AVAILABLE && gStateOut == LocationGroupState.AVAILABLE && (this.stateOutLocker == null || this.stateOutLocker.equals(lockLg))) {
this.groupStateOut = gStateOut;
this.stateOutLocker = null;
for (LocationGroup child : locationGroups) {
child.changeGroupStateOut(gStateOut, lockLg);
}
}
if (this.groupStateOut == LocationGroupState.AVAILABLE && gStateOut == LocationGroupState.NOT_AVAILABLE && (this.stateOutLocker == null || this.stateOutLocker.equals(lockLg))) {
this.groupStateOut = gStateOut;
this.stateOutLocker = lockLg;
for (LocationGroup child : locationGroups) {
child.changeGroupStateOut(gStateOut, lockLg);
}
}
}
/**
* Returns the count of all sub {@link Location}s.
*
* @return The count of {@link Location}s belonging to this {@code LocationGroup}
*/
public int getNoLocations() {
return this.locations != null ? this.locations.size() : 0;
}
/**
* Returns the maximum fill level of the {@code LocationGroup}.<br> The maximum fill level defines how many {@link Location}s of the
* {@code LocationGroup} can be occupied by {@code TransportUnit}s. <p> The maximum fill level is a value between 0 and 1 and represents
* a percentage value. </p>
*
* @return The maximum fill level
*/
public float getMaxFillLevel() {
return this.maxFillLevel;
}
/**
* Set the maximum fill level for the {@code LocationGroup}. <p> Pass a value between 0 and 1.<br> For example maxFillLevel = 0.85
* means: 85% of all {@link Location}s can be occupied. </p>
*
* @param maxFillLevel The maximum fill level
*/
public void setMaxFillLevel(float maxFillLevel) {
this.maxFillLevel = maxFillLevel;
}
/**
* Returns the type of the {@code LocationGroup}.
*
* @return The type of the {@code LocationGroup}
*/
public String getGroupType() {
return this.groupType;
}
/**
* Set the type for the {@code LocationGroup}.
*
* @param groupType The type of the {@code LocationGroup}
*/
public void setGroupType(String groupType) {
this.groupType = groupType;
}
/**
* Returns the description text.
*
* @return The Description as String
*/
public String getDescription() {
return this.description;
}
/**
* Set the description text.
*
* @param description The String to set as description text
*/
public void setDescription(String description) {
this.description = description;
}
/**
* Returns the parent {@code LocationGroup}.
*
* @return The parent {@code LocationGroup}
*/
public LocationGroup getParent() {
return this.parent;
}
/**
* Sets the parent {@code LocationGroup} of this {@code LocationGroup}.
*
* @param parent The parent {@code LocationGroup} to set
*/
public void setParent(LocationGroup parent) {
this.parent = parent;
}
/**
* Return all child {@code LocationGroup}.
*
* @return A set of all {@code LocationGroup} having this one as parent
*/
public Set<LocationGroup> getLocationGroups() {
return locationGroups;
}
/**
* Check whether this {@code LocationGroup} has {@code LocationGroup}s as children.
*
* @return {@literal true} if {@code LocationGroup}s are assigned, otherwise {@literal false}
*/
public boolean hasLocationGroups() {
return locationGroups != null && !locationGroups.isEmpty();
}
/**
* Sets the child {@code LocationGroup}.
*
* @param locationGroups the set of LocationGroups to set
*/
public void setLocationGroups(Set<LocationGroup> locationGroups) {
this.locationGroups = locationGroups;
}
/**
* Add a {@code LocationGroup} to the list of children.
*
* @param locationGroup The {@code LocationGroup} to be added as a child
* @return {@literal true} if the {@code LocationGroup} was new in the collection of {@code LocationGroup}s, otherwise {@literal false}
*/
public boolean addLocationGroup(LocationGroup locationGroup) {
if (locationGroup == null) {
throw new IllegalArgumentException("LocationGroup to be added is null");
}
if (locationGroup.parent != null) {
locationGroup.parent.removeLocationGroup(locationGroup);
}
locationGroup.parent = this;
locationGroup.changeGroupStateIn(groupStateIn, this);
locationGroup.changeGroupStateOut(groupStateOut, this);
return locationGroups.add(locationGroup);
}
/**
* Remove a {@code LocationGroup} from the list of children.
*
* @param locationGroup The {@code LocationGroup} to be removed from the list of children
* @return {@literal true} if the {@code LocationGroup} was found and could be removed, otherwise {@literal false}
*/
public boolean removeLocationGroup(@NotNull LocationGroup locationGroup) {
Assert.notNull(locationGroup, () -> "LocationGroup to remove is null. this: " + this);
locationGroup.parent = null;
return locationGroups.remove(locationGroup);
}
/**
* Return all {@link Location}s.
*
* @return {@link Location}s
*/
public Set<Location> getLocations() {
return locations;
}
/**
* Check whether this {@code LocationGroup} has {@code Location}s assigned.
*
* @return {@literal true} if {@code Location}s are assigned, otherwise {@literal false}
*/
public boolean hasLocations() {
return locations != null && !locations.isEmpty();
}
/**
* Add a {@link Location} to the list of children.
*
* @param location The {@link Location} to be added as child
* @return {@literal true} if the {@link Location} was new in the collection of {@link Location}s, otherwise {@literal false}
*/
public boolean addLocation(Location location) {
Assert.notNull(location, () -> "Location to be added to LocationGroup is null. this: " + this);
location.setLocationGroup(this);
return locations.add(location);
}
/**
* Remove a {@link Location} from the list of children.
*
* @param location The {@link Location} to be removed from the list of children
* @return {@literal true} if the {@link Location} was found and could be removed, otherwise {@literal false}
*/
public boolean removeLocation(Location location) {
Assert.notNull(location, () -> "Location to remove from LocationGroup is null. this: " + this);
location.unsetLocationGroup();
return locations.remove(location);
}
/**
* Returns the locationGroupCountingActive.
*
* @return The locationGroupCountingActive
*/
public boolean isLocationGroupCountingActive() {
return locationGroupCountingActive;
}
/**
* Set the locationGroupCountingActive.
*
* @param locationGroupCountingActive The locationGroupCountingActive to set
*/
public void setLocationGroupCountingActive(boolean locationGroupCountingActive) {
this.locationGroupCountingActive = locationGroupCountingActive;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 111;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof LocationGroup)) {
return false;
}
LocationGroup other = (LocationGroup) obj;
if (name == null) {
if (other.name != null) {
return false;
}
} else if (!name.equals(other.name)) {
return false;
}
return true;
}
/**
* Return the name of the {@code LocationGroup} as String.
*
* @return The name
*/
@Override
public String toString() {
return getName();
}
/**
* Tries to change the {@code groupStateIn} and {@code groupStateOut} of the {@code LocationGroup}. A state change is only allowed when
* the parent {@code LocationGroup}s state is not blocked.
*
* @param stateIn The new groupStateIn to set, or {@literal null}
* @param stateOut The new groupStateOut to set, or {@literal null}
*/
public void changeState(LocationGroupState stateIn, LocationGroupState stateOut) {
if (groupStateIn != stateIn && stateIn != null) {
// GroupStateIn changed
if (parent != null && parent.getGroupStateIn() == LocationGroupState.NOT_AVAILABLE && groupStateIn == LocationGroupState.AVAILABLE) {
throw new ServiceLayerException("Not allowed to change GroupStateIn, parent locationGroup is not available");
}
changeGroupStateIn(stateIn, this);
}
if (groupStateOut != stateOut && stateOut != null) {
// GroupStateOut changed
if (parent != null && parent.getGroupStateOut() == LocationGroupState.NOT_AVAILABLE && groupStateOut == LocationGroupState.AVAILABLE) {
throw new ServiceLayerException("Not allowed to change GroupStateOut, parent locationGroup is not available");
}
changeGroupStateOut(stateOut, this);
}
}
/**
* Whether this LocationGroup has a parent LocationGroup or not.
*
* @return {@literal true} If it has a parent
*/
public boolean hasParent() {
return parent != null;
}
}