All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.erudika.para.core.User Maven / Gradle / Ivy

There is a newer version: 1.50.3
Show newest version
/*
 * Copyright 2013-2017 Erudika. https://erudika.com
 *
 * 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.
 *
 * For issues and patches go to: https://github.com/erudika
 */
package com.erudika.para.core;

import com.erudika.para.core.utils.CoreUtils;
import com.erudika.para.core.utils.ParaObjectUtils;
import com.eaio.uuid.UUID;
import com.erudika.para.annotations.Email;
import com.erudika.para.annotations.Locked;
import com.erudika.para.annotations.Stored;
import com.erudika.para.i18n.CurrencyUtils;
import com.erudika.para.utils.Config;
import com.erudika.para.utils.Pager;
import com.erudika.para.utils.Utils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.validator.constraints.NotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The core user object. Stores information about users.
 * @author Alex Bogdanovski [[email protected]]
 */
public class User implements ParaObject {
	private static final long serialVersionUID = 1L;
	private static Logger logger = LoggerFactory.getLogger(User.class);

	@Stored @Locked private String id;
	@Stored @Locked private Long timestamp;
	@Stored @Locked private String type;
	@Stored @Locked private String appid;
	@Stored @Locked private String parentid;
	@Stored @Locked private String creatorid;
	@Stored private Long updated;
	@Stored private String name;
	@Stored private List tags;
	@Stored private Integer votes;
	@Stored private Boolean stored;
	@Stored private Boolean indexed;
	@Stored private Boolean cached;

	@Stored @NotBlank private String identifier;
	@Stored @Locked @NotBlank private String groups;
	@Stored private Boolean active;
	@Stored @NotBlank @Email private String email;
	@Stored private String currency;
	@Stored private String picture;
	@Stored private String lastIp;
	@Stored @Locked private String tokenSecret;

	private transient String password;

	/**
	 * No-args constructor.
	 */
	public User() {
		this(null);
	}

	/**
	 * Default constructor.
	 * @param id the id
	 */
	public User(String id) {
		setId(id);
		setName(getName());
		this.groups = Groups.USERS.toString();
	}

	/**
	 * Token secret - used for generating JWT tokens.
	 * Changing this secret would invalidate all existing user tokens.
	 * A kind of global "logout".
	 * @return a random string
	 */
	@JsonIgnore
	public String getTokenSecret() {
		if (tokenSecret == null) {
			resetTokenSecret();
		}
		return tokenSecret;
	}

	/**
	 * Sets the token secret.
	 * @param tokenSecret a random string
	 */
	public void setTokenSecret(String tokenSecret) {
		this.tokenSecret = tokenSecret;
	}

	/**
	 *	The IP address of the user recorded on last login.
	 * @return the IP or null
	 */
	@JsonIgnore
	public String getLastIp() {
		return lastIp;
	}

	/**
	 * Sets the IP of the user.
	 * @param lastIp last known IP address
	 */
	public void setLastIp(String lastIp) {
		this.lastIp = lastIp;
	}

	/**
	 * The profile picture URL.
	 * @return a URL or null
	 */
	public String getPicture() {
		return StringUtils.isBlank(picture) ?
				"https://www.gravatar.com/avatar?d=mm&size=400" : picture;
	}

	/**
	 * Sets the profile picture URL.
	 * @param picture the picture URL.
	 */
	public void setPicture(String picture) {
		this.picture = picture;
	}

	/**
	 * Returns true if this account is active.
	 * @return true if active
	 */
	public Boolean getActive() {
		if (active == null) {
			active = false;
		}
		return active;
	}

	/**
	 * Sets the account active.
	 * @param active true if active
	 */
	public void setActive(Boolean active) {
		this.active = active;
	}

	/**
	 * Returns the security groups for this user.
	 * @return the groups string
	 */
	public String getGroups() {
		return groups;
	}

	/**
	 * Sets the security groups for this user.
	 * @param groups the groups string
	 */
	public void setGroups(String groups) {
		this.groups = groups;
	}

	/**
	 * Returns the main identifier for this user.
	 * An identifier is basically a unique username that identifies a user.
	 * @return the main identifier
	 */
	public String getIdentifier() {
		return identifier;
	}

	/**
	 * Sets the main identifier.
	 * @param identifier the main identifier
	 */
	public void setIdentifier(String identifier) {
		this.identifier = identifier;
	}

	/**
	 * The user's email.
	 * @return email
	 */
	public String getEmail() {
		return email;
	}

	/**
	 * Sets the email.
	 * @param email email
	 */
	public void setEmail(String email) {
		this.email = email;
	}

	/**
	 * The user's currency preference.
	 * @return a currency 3-letter code in uppercase
	 */
	public String getCurrency() {
		return currency;
	}

	/**
	 * Sets a preferred currency. Default is "EUR".
	 * @param currency a 3-letter currency code
	 */
	public void setCurrency(String currency) {
		currency = StringUtils.upperCase(currency);
		if (!CurrencyUtils.getInstance().isValidCurrency(currency)) {
			currency = "EUR";
		}
		this.currency = currency;
	}

	/**
	 * Generates a new token secret.
	 * This is whould be equivalent to "logout everywhere".
	 */
	public void resetTokenSecret() {
		tokenSecret = Utils.generateSecurityToken();
	}

	/**
	 * Note: this method assumes that child objects can be modified by their parents.
	 * This might not work for special cases where a parent has no rights over a child.
	 * @param obj an object
	 * @return true if the user is the creator or parent of this object or an admin user
	 */
	public boolean canModify(ParaObject obj) {
		if (obj == null || id == null) {
			return false;
		} else {
			boolean isCreatedByMe = obj.getCreatorid() != null &&
					(obj.getCreatorid().startsWith(id + Config.SEPARATOR) || id.equals(obj.getCreatorid()));
			boolean mine = isCreatedByMe || id.equals(obj.getId()) || id.equals(obj.getParentid());
			return (mine || isAdmin());
		}
	}

	@Override
	public String create() {
		if (StringUtils.isBlank(getIdentifier())) {
			return null;
		}
		if (!StringUtils.isBlank(getPassword()) && getPassword().length() < Config.MIN_PASS_LENGTH) {
			return null;
		}

		// admin detected
		if (!Config.ADMIN_IDENT.isEmpty() && Config.ADMIN_IDENT.equals(getIdentifier())) {
			setGroups(User.Groups.ADMINS.toString());
		} else {
			setGroups(User.Groups.USERS.toString());
		}

		setGravatarPicture();

		if (StringUtils.isBlank(tokenSecret)) {
			resetTokenSecret();
		}

		if (CoreUtils.getInstance().getDao().create(getAppid(), this) != null) {
			createIdentifier(getId(), getIdentifier(), getPassword());
		}

		return getId();
	}

	@Override
	public void delete() {
		if (getId() != null) {
			CoreUtils.getInstance().getDao().deleteAll(getAppid(), getIdentifiers());
			CoreUtils.getInstance().getDao().delete(getAppid(), this);
		}
	}

	/**
	 * Returns a list of identifiers for this user (can have many).
	 * @return a list of {@link Sysprop} objects
	 */
	@JsonIgnore
	public List getIdentifiers() {
		return CoreUtils.getInstance().getSearch().findTerms(getAppid(), Utils.type(Sysprop.class),
				Collections.singletonMap(Config._CREATORID, getId()), true);
	}

	/**
	 * Attaches a new identifier to this user.
	 * @param identifier a new identifier
	 */
	public void attachIdentifier(String identifier) {
		if (this.exists()) {
			createIdentifier(getId(), identifier);
		}
	}

	/**
	 * Detaches a secondary identifier which is not already used by this user.
	 * @param identifier an attached identifier
	 */
	public void detachIdentifier(String identifier) {
		if (!StringUtils.equals(identifier, getIdentifier())) {
			Sysprop s = CoreUtils.getInstance().getDao().read(getAppid(), identifier);
			if (s != null && StringUtils.equals(getId(), s.getCreatorid())) {
				deleteIdentifier(identifier);
			}
		}
	}

	/**
	 * Is the main identifier a Facebook id.
	 * @return true if user is signed in with Facebook
	 */
	@JsonIgnore
	public boolean isFacebookUser() {
		return StringUtils.startsWithIgnoreCase(identifier, Config.FB_PREFIX);
	}

	/**
	 * Is the main identifier a Google+ id.
	 * @return true if user is signed in with Google+
	 */
	@JsonIgnore
	public boolean isGooglePlusUser() {
		return StringUtils.startsWithIgnoreCase(identifier, Config.GPLUS_PREFIX);
	}

	/**
	 * Is the main identifier a LinkedIn id.
	 * @return true if user is signed in with LinkedIn
	 */
	@JsonIgnore
	public boolean isLinkedInUser() {
		return StringUtils.startsWithIgnoreCase(identifier, Config.LINKEDIN_PREFIX);
	}

	/**
	 * Is the main identifier a Twitter id.
	 * @return true if user is signed in with Twitter
	 */
	@JsonIgnore
	public boolean isTwitterUser() {
		return StringUtils.startsWithIgnoreCase(identifier, Config.TWITTER_PREFIX);
	}

	/**
	 * Is the main identifier a GitHub id.
	 * @return true if user is signed in with GitHub
	 */
	@JsonIgnore
	public boolean isGitHubUser() {
		return StringUtils.startsWithIgnoreCase(identifier, Config.GITHUB_PREFIX);
	}

	/**
	 * Is the main identifier a Microsoft/Windows account id.
	 * @return true if user is signed in with a Microsoft account
	 */
	@JsonIgnore
	public boolean isMicrosoftUser() {
		return StringUtils.startsWithIgnoreCase(identifier, Config.MICROSOFT_PREFIX);
	}

	/**
	 * Checks for admin rights.
	 * @return true if user has admin rights
	 */
	@JsonIgnore
	public boolean isAdmin() {
		return StringUtils.equalsIgnoreCase(this.groups, Groups.ADMINS.toString());
	}

	/**
	 * Checks for moderator rights.
	 * @return true if user has mod rights
	 */
	@JsonIgnore
	public boolean isModerator() {
		return isAdmin() ? true : StringUtils.equalsIgnoreCase(this.groups, Groups.MODS.toString());
	}

	/**
	 * Returns the name of the identity provider.
	 * @return "facebook", "google"... etc.
	 */
	public String getIdentityProvider() {
		if (isFacebookUser()) {
			return "facebook";
		} else if (isGooglePlusUser()) {
			return "google";
		} else if (isGitHubUser()) {
			return "github";
		} else if (isTwitterUser()) {
			return "twitter";
		} else if (isLinkedInUser()) {
			return "linkedin";
		} else {
			return "none";
		}
	}

	/**
	 * The password. A transient field used for validation.
	 * @return the password.
	 */
	@JsonIgnore
	public String getPassword() {
		return password;
	}

	/**
	 * Sets a password.
	 * @param password a password
	 */
	public void setPassword(String password) {
		this.password = password;
	}

	/**
	 * Returns a user object for a given identifier.
	 * @param u a user having a valid identifier set.
	 * @return a user or null if no user is found for this identifier
	 */
	public static final User readUserForIdentifier(User u) {
		if (u == null || StringUtils.isBlank(u.getIdentifier())) {
			return null;
		}
		String identifier = u.getIdentifier();
		Sysprop s = CoreUtils.getInstance().getDao().read(u.getAppid(), identifier);
		if (s != null && s.getCreatorid() != null) {
			User user = CoreUtils.getInstance().getDao().read(u.getAppid(), s.getCreatorid());
			if (user != null) {
				if (!identifier.equals(user.getIdentifier())) {
					logger.info("Identifier changed for user '{}', from {} to {}.",
							user.getId(), user.getIdentifier(), identifier);
					// the main identifier was changed - update
					user.setIdentifier(identifier);
					CoreUtils.getInstance().getDao().update(user.getAppid(), user);
				}
				user.setPassword((String) s.getProperty(Config._PASSWORD));
				return user;
			}
		}
		logger.info("User not found for identifier {}/{}, {}.", u.getAppid(), identifier, u.getId());
		return null;
	}

	/**
	 * Checks if a user has entered the correct password.
	 * Compares password hashes.
	 * @param u a user with a set password
	 * @return true if password matches the one in the data store
	 */
	public static final boolean passwordMatches(User u) {
		if (u == null) {
			return false;
		}
		String password = u.getPassword();
		String identifier = u.getIdentifier();
		if (StringUtils.isBlank(password) || StringUtils.isBlank(identifier)) {
			return false;
		}
		ParaObject s = CoreUtils.getInstance().getDao().read(u.getAppid(), identifier);
		if (s != null) {
			if (s instanceof Sysprop) {
				String storedHash = (String) ((Sysprop) s).getProperty(Config._PASSWORD);
				return Utils.bcryptMatches(password, storedHash);
			} else {
				LoggerFactory.getLogger(User.class).
						warn(Utils.formatMessage("Failed to read auth object for user '{}' using identifier '{}'.",
								u.getId(), identifier));
			}
		}
		return false;
	}

	/**
	 * Generates a new password reset token. Sent via email for pass reset.
	 * @return the pass reset token
	 */
	public final String generatePasswordResetToken() {
		if (StringUtils.isBlank(identifier)) {
			return "";
		}
		Sysprop s = CoreUtils.getInstance().getDao().read(getAppid(), identifier);
		if (s != null) {
			String token = Utils.generateSecurityToken(42, true);
			s.addProperty(Config._RESET_TOKEN, token);
			CoreUtils.getInstance().getDao().update(getAppid(), s);
			return token;
		}
		return "";
	}

	/**
	 * Changes the user password permanently.
	 * @param token the reset token. see {@link #generatePasswordResetToken()}
	 * @param newpass the new password
	 * @return true if successful
	 */
	public final boolean resetPassword(String token, String newpass) {
		if (StringUtils.isBlank(newpass) || StringUtils.isBlank(token) || newpass.length() < Config.MIN_PASS_LENGTH) {
			return false;
		}
		Sysprop s = CoreUtils.getInstance().getDao().read(getAppid(), identifier);
		if (isValidToken(s, Config._RESET_TOKEN, token)) {
			s.removeProperty(Config._RESET_TOKEN);
			String hashed = Utils.bcrypt(newpass);
			s.addProperty(Config._PASSWORD, hashed);
			setPassword(hashed);
			CoreUtils.getInstance().getDao().update(getAppid(), s);
			return true;
		}
		return false;
	}

	private boolean createIdentifier(String userid, String newIdent) {
		return createIdentifier(userid, newIdent, null);
	}

	/**
	 * Creates a new identifier object using {@link Sysprop}.
	 * Used for identifying a user when signing in.
	 * @param userid a user id
	 * @param newIdent a new identifier
	 * @param password a password for the user (optional)
	 * @return true if successful
	 */
	private boolean createIdentifier(String userid, String newIdent, String password) {
		if (StringUtils.isBlank(userid) || StringUtils.isBlank(newIdent)) {
			return false;
		}
		Sysprop s = new Sysprop();
		s.setId(newIdent);
		s.setName(Config._IDENTIFIER);
		s.setCreatorid(userid);
		if (!StringUtils.isBlank(password)) {
			String hashed = Utils.bcrypt(password);
			s.addProperty(Config._PASSWORD, hashed);
			setPassword(hashed);
		}
		return CoreUtils.getInstance().getDao().create(getAppid(), s) != null;
	}

	/**
	 * Deletes the identifier and the user can no longer sign in with it.
	 * @param ident the attached identifier
	 */
	private void deleteIdentifier(String ident) {
		if (!StringUtils.isBlank(ident)) {
			CoreUtils.getInstance().getDao().delete(getAppid(), new Sysprop(ident));
		}
	}

	/**
	 * Generates a new email confirmation token. Sent via email for user activation.
	 * @return a Base64 encoded UUID
	 */
	public String generateEmailConfirmationToken() {
		if (StringUtils.isBlank(identifier)) {
			return "";
		}
		Sysprop s = CoreUtils.getInstance().getDao().read(getAppid(), identifier);
		if (s != null) {
			String token = Utils.base64encURL(new UUID().toString().getBytes());
			s.addProperty(Config._EMAIL_TOKEN, token);
			CoreUtils.getInstance().getDao().update(getAppid(), s);
			return token;
		}
		return "";
	}

	/**
	 * Activates a user if a given token matches the one stored.
	 * @param token the email confirmation token. see {@link #generateEmailConfirmationToken() }
	 * @return true if successful
	 */
	public final boolean activateWithEmailToken(String token) {
		Sysprop s = CoreUtils.getInstance().getDao().read(getAppid(), identifier);
		if (isValidToken(s, Config._EMAIL_TOKEN, token)) {
			s.removeProperty(Config._EMAIL_TOKEN);
			CoreUtils.getInstance().getDao().update(getAppid(), s);
			setActive(true);
			update();
			return true;
		}
		return false;
	}

	/**
	 * Validates a token sent via email for password reset.
	 * @param token a token
	 * @return true if valid
	 */
	public final boolean isValidPasswordResetToken(String token) {
		Sysprop s = CoreUtils.getInstance().getDao().read(getAppid(), identifier);
		return isValidToken(s, Config._RESET_TOKEN, token);
	}

	/**
	 * Validates a token sent for email confirmation.
	 * @param token a token
	 * @return true if valid
	 */
	public final boolean isValidEmailConfirmationToken(String token) {
		Sysprop s = CoreUtils.getInstance().getDao().read(getAppid(), identifier);
		return isValidToken(s, Config._EMAIL_TOKEN, token);
	}

	private boolean isValidToken(Sysprop s, String key, String token) {
		if (StringUtils.isBlank(token)) {
			return false;
		}
		if (s != null && s.hasProperty(key)) {
			String storedToken = (String) s.getProperty(key);
			// tokens expire afer a reasonably short period ~ 30 mins
			long timeout = Config.PASSRESET_TIMEOUT_SEC * 1000;
			if (StringUtils.equals(storedToken, token) && (s.getUpdated() + timeout) > Utils.timestamp()) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Sets the profile picture using the Gravatar service.
	 */
	private void setGravatarPicture() {
		if (StringUtils.isBlank(picture)) {
			if (email != null) {
				String emailHash = Utils.md5(email);
				setPicture("https://www.gravatar.com/avatar/" + emailHash + "?size=400&d=mm&r=pg");
			} else {
				setPicture("https://www.gravatar.com/avatar?d=mm&size=400");
			}
		}
	}

	/**
	 * Simple groups enum.
	 */
	public enum Groups {
		/**
		 * The standard user group.
		 */
		USERS,
		/**
		 * Moderators group.
		 */
		MODS,
		/**
		 * Administrators group.
		 */
		ADMINS;

		@Override
		public String toString() {
			return this.name().toLowerCase();
		}
	}

	/**
	 * Simple user roles enum.
	 */
	public enum Roles {
		/**
		 * The standard role.
		 */
		USER,
		/**
		 * The moderator role.
		 */
		MOD,
		/**
		 * The administrator role.
		 */
		ADMIN;

		@Override
		public String toString() {
			return "ROLE_".concat(this.name());
		}
	}

	////////////////////////////////////////////////////////

	@Override
	public final String getId() {
		return id;
	}

	@Override
	public final void setId(String id) {
		this.id = id;
	}

	@Override
	public final String getType() {
		type = (type == null) ? Utils.type(this.getClass()) : type;
		return type;
	}

	@Override
	public final void setType(String type) {
		this.type = type;
	}

	@Override
	public String getAppid() {
		appid = (appid == null) ? Config.APP_NAME_NS : appid;
		return appid;
	}

	@Override
	public void setAppid(String appid) {
		this.appid = appid;
	}

	@Override
	public String getObjectURI() {
		return CoreUtils.getInstance().getObjectURI(this);
	}

	@Override
	public List getTags() {
		return tags;
	}

	@Override
	public void setTags(List tags) {
		this.tags = tags;
	}

	@Override
	public Boolean getStored() {
		if (stored == null) {
			stored = true;
		}
		return stored;
	}

	@Override
	public void setStored(Boolean stored) {
		this.stored = stored;
	}

	@Override
	public Boolean getIndexed() {
		if (indexed == null) {
			indexed = true;
		}
		return indexed;
	}

	@Override
	public void setIndexed(Boolean indexed) {
		this.indexed = indexed;
	}

	@Override
	public Boolean getCached() {
		if (cached == null) {
			cached = true;
		}
		return cached;
	}

	@Override
	public void setCached(Boolean cached) {
		this.cached = cached;
	}

	@Override
	public Long getTimestamp() {
		return (timestamp != null && timestamp != 0) ? timestamp : null;
	}

	@Override
	public void setTimestamp(Long timestamp) {
		this.timestamp = timestamp;
	}

	@Override
	public String getCreatorid() {
		return creatorid;
	}

	@Override
	public void setCreatorid(String creatorid) {
		this.creatorid = creatorid;
	}

	@Override
	public final String getName() {
		return CoreUtils.getInstance().getName(name, id);
	}

	@Override
	public final void setName(String name) {
		this.name = (name == null || !name.isEmpty()) ? name : this.name;
	}

	@Override
	public String getPlural() {
		return Utils.singularToPlural(getType());
	}

	@Override
	public String getParentid() {
		return parentid;
	}

	@Override
	public void setParentid(String parentid) {
		this.parentid = parentid;
	}

	@Override
	public Long getUpdated() {
		return (updated != null && updated != 0) ? updated : null;
	}

	@Override
	public void setUpdated(Long updated) {
		this.updated = updated;
	}

	@Override
	public void update() {
		CoreUtils.getInstance().getDao().update(getAppid(), this);
	}

	@Override
	public boolean exists() {
		return CoreUtils.getInstance().getDao().read(getAppid(), getId()) != null;
	}

	@Override
	public boolean voteUp(String userid) {
		return CoreUtils.getInstance().vote(this, userid, VoteValue.UP);
	}

	@Override
	public boolean voteDown(String userid) {
		return CoreUtils.getInstance().vote(this, userid, VoteValue.DOWN);
	}

	@Override
	public Integer getVotes() {
		return (votes == null) ? 0 : votes;
	}

	@Override
	public void setVotes(Integer votes) {
		this.votes = votes;
	}

	@Override
	public Long countLinks(String type2) {
		return CoreUtils.getInstance().countLinks(this, type2);
	}

	@Override
	public List getLinks(String type2, Pager... pager) {
		return CoreUtils.getInstance().getLinks(this, type2, pager);
	}

	@Override
	public 

List

getLinkedObjects(String type, Pager... pager) { return CoreUtils.getInstance().getLinkedObjects(this, type, pager); } @Override public

List

findLinkedObjects(String type, String field, String query, Pager... pager) { return CoreUtils.getInstance().findLinkedObjects(this, type, field, query, pager); } @Override public boolean isLinked(String type2, String id2) { return CoreUtils.getInstance().isLinked(this, type2, id2); } @Override public boolean isLinked(ParaObject toObj) { return CoreUtils.getInstance().isLinked(this, toObj); } @Override public String link(String id2) { return CoreUtils.getInstance().link(this, id2); } @Override public void unlink(String type, String id2) { CoreUtils.getInstance().unlink(this, type, id2); } @Override public void unlinkAll() { CoreUtils.getInstance().unlinkAll(this); } @Override public Long countChildren(String type) { return CoreUtils.getInstance().countChildren(this, type); } @Override public

List

getChildren(String type, Pager... pager) { return CoreUtils.getInstance().getChildren(this, type, pager); } @Override public

List

getChildren(String type, String field, String term, Pager... pager) { return CoreUtils.getInstance().getChildren(this, type, field, term, pager); } @Override public

List

findChildren(String type, String query, Pager... pager) { return CoreUtils.getInstance().findChildren(this, type, query, pager); } @Override public void deleteChildren(String type) { CoreUtils.getInstance().deleteChildren(this, type); } @Override public int hashCode() { int hash = 7; hash = 67 * hash + Objects.hashCode(this.id) + Objects.hashCode(this.name); return hash; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final ParaObject other = (ParaObject) obj; if (!Objects.equals(this.id, other.getId())) { return false; } return true; } @Override public String toString() { return ParaObjectUtils.toJSON(this); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy