I have a DTO interface which fetches data from different tables using joins. I have made a DTO interface with the abstract getter methods something like this.
public interface HRJobsDTO {
String getEditorName();
String getEditorId();
String getBillingMonth();
Integer getEditorWordCount();
Integer getJobCount();
Integer getEmployeeGrade();
Float getGrossPayableAmount();
Float getJobBillingRate();
Float getTaxDeduction();
Float getTaxDeductionAmount();
Float getNetPayableAmount();
String getInvoiceStatus();
String getFreelanceInvoiceId();
}
In this interface my getFreelanceInvoiceId(); method returns a JSON Array using json_arrayagg function of mysql. I changed the datatype to String, String[] and Arraylist but it returns something like this in my response
"freelanceInvoiceId": "[\"4af9e342-065b-4594-9f4f-a408d5db9819/2022121-95540\", \"4af9e342-065b-4594-9f4f-a408d5db9819/2022121-95540\", \"4af9e342-065b-4594-9f4f-a408d5db9819/20221215-53817\", \"4af9e342-065b-4594-9f4f-a408d5db9819/20221215-53817\", \"4af9e342-065b-4594-9f4f-a408d5db9819/20221215-53817\"]"
Is there any way to return only array with exclusion of backslashes?
You can use #Converter from JPA (implemented by hibernate also)
#Converter
public class List2StringConveter implements AttributeConverter<List<String>, String> {
#Override
public String convertToDatabaseColumn(List<String> attribute) {
if (attribute == null || attribute.isEmpty()) {
return "";
}
return StringUtils.join(attribute, ",");
}
#Override
public List<String> convertToEntityAttribute(String dbData) {
if (dbData == null || dbData.trim().length() == 0) {
return new ArrayList<String>();
}
String[] data = dbData.split(",");
return Arrays.asList(data);
}
}
And references it in the pojo class as below
#Column(name="freeLanceInvoiceId")
#Convert(converter = List2StringConveter.class)
private List<String> tags=new ArrayList<>();
Basically I have list of names separated by camma (;),
CREATE TABLE familynames (
id int4 NOT NULL DEFAULT nextval('family_id_seq'::regclass),
names varchar NULL
);
INSERT INTO familynames (id, names) VALUES(1, 'Animalia;Arthropoda;Pancrustacea;Hexapoda');
The converter :
#Converter
public class NameConverter implements AttributeConverter<List<String>, String> {
#Override
public List<String> convertToEntityAttribute(String attribute) {
if (attribute == null) {
return null;
}
List<String> namesList = new ArrayList<String>(Arrays.asList(attribute.split(";")));
return namesList;
}
#Override
public String convertToDatabaseColumn(List<String> namesList) {
if (namesList == null || namesList.size() == 0) {
return null;
}
String result = namesList.stream().collect( Collectors.joining( ";" ) );
return result;
}
}
An entity that map the sql table
#Entity
#Table(name = "familynames")
#Data
#NoArgsConstructor
#AllArgsConstructor
public class FamilyNames implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
#Convert(converter = NameConverter.class, attributeName = "names")
private List<String> names;
}
An interface for projection
public interface FamilyNamesDto {
Integer getId();
List<String> getNames();
}
The repository that ensure the selection and the projection
#Repository
public interface FamilyNamesRepository extends JpaRepository<FamilyNames, Long> {
/** #return the whole elements of mapped elements as dto */
List<FamilyNamesDto> findAllProjectedBy();
}
How to autowire it
#Autowired
private FamilyNamesRepository familyNamesRepository;
how to call repository within any service
List<FamilyNamesDto> fs = familyNamesRepository.findAllProjectedBy();
results :
Related
This is a complex problem i'm gonna try describe step by step
what i'm trying to achieve is this query
SELECT FUNCTION(GetTaskStatus(t.due_date, t.completed_date)) as status, count(*) as total FROM tasks t GROUP BY status
where GetTaskStatus is a stored function
In my java code TaskStatus is an enum:
public enum TaskStatus {
COMPLETED(2), LATE_COMPLETE(14), SCHEDULED(3), DUE_TODAY(5), LATE(7), OTHER(11);
private final int code;
TaskStatus(int i) {
code = i;
}
#javax.persistence.Converter(autoApply = true)
public static final class Converter implements AttributeConverter<TaskStatus, Integer> {
#Override
public Integer convertToDatabaseColumn(TaskStatus attribute) {
if (attribute == null)
return null;
return attribute.code;
}
#Override
public TaskStatus convertToEntityAttribute(Integer dbData) {
return fromInteger(dbData);
}
public static TaskStatus fromInteger(Integer dbData) {
return dbData == null || dbData == 0 ? null : Arrays.stream(values()).filter(taskStatus -> taskStatus.code == dbData).findFirst().orElse(OTHER);
}
}
note that i've a converter for that so hibernate should be able to deserialize that.
then my dto
#Getter
#Setter
#ToString
#NoArgsConstructor
#AllArgsConstructor
#Builder
public class TaskMetricsDto {
#JsonProperty("status")
private TaskStatus status;
#JsonProperty("total")
private int total;
public TaskMetricsDto(#Nullable Integer statusCode, #Nullable Long total) {
status = TaskStatus.Converter.fromInteger(statusCode);
if (total != null)
this.total = total.intValue();
}
public void setStatus(Integer integer) {
this.status = TaskStatus.Converter.fromInteger(integer);
}
public void setTotal(Long total) {
if (total != null)
this.total = total.intValue();
}
}
then we go repository
#Repository
public interface TaskRepository extends JpaRepository<Task, Long> {
#Query("SELECT new br.com.fisgar.crm.dtos.TaskMetricsDto(CAST(GetTaskStatus(t.dueDate, t.completedDate) as java.lang.Integer) as status, count(*)) FROM Task t WHERE t.board.id IN (:boardIds) GROUP BY status")
public List<TaskMetricsDto> findMetrics(#Param("boardIds") Set<Long> boardIds);
}
This query is working, but in my opnion is much verbose and overcomplicated compared to the original sql query... all this due many problems I had
PROBLEM 1:
if i do
#Query("SELECT CAST(GetTaskStatus(t.dueDate, t.completedDate) as java.lang.Integer) as status, count(*) FROM Task t WHERE t.board.id IN (:boardIds) GROUP BY status")
without the explicit new, the query fails
"message": "CONVERTERNOTFOUNDEXCEPTION: No converter found capable of converting from type [org.springframework.data.jpa.repository.query.AbstractJpaQuery$TupleConverter$TupleBackedMap] to type [br.com.fisgar.crm.dtos.TaskMetricsDto]",
but my dto has exact two fields and even has a constructor which takes an int and a long... even getter and setter for that...
according to spring manual this should work
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections
PROBLEM 2:
altought i have an converter for TaskStatus - Integer, hiberna
I'm trying to expand on this Baeldung tutorial https://www.baeldung.com/rest-api-search-language-spring-data-specifications
But I want the Specification to be Generic and I wanted to allow the client to search by values of embedded objects. Everything works for String and some numbers, but not for ids and other more complicated objects like Date.
My Model: (assume a person can only have 1 pet)
#Entity
public Person {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private ID id;
private String name;
private Date dateOfBirth
private Integer age;
private Pet pet;
// Getter & Setters etc
}
#Entity
public Pet {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private ID id;
private String type;
private String name;
private Integer numOfLegs;
// Getter & Setters etc
}
Person repository:
#Repository
public interface PersonRepository extends JpaRepository<Person, Integer>, JpaSpecificationExecutor<Person>{}
Search Criteria that will hold the key, operator and value that we can search by.
public class EntitySearchCriteria {
private String key;
private String operation;
private Object value;
public EntitySearchCriteria(final String key, final String operation, final Object value) {
this.key = key;
this.operation = operation;
this.value = value;
}
// Getters and Setters etc
My Generic Specification class (this is really where the action is to build the predicates that are to be used). This also allows the client to set a SearchCriteria on a value of a joined table. e.g. "Pet.name=Muffins"
public abstract class AbstractEntitySpecification<T, ID extends Serializable> implements Specification<T> {
protected EntitySearchCriteria criteria;
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder) {
if (criteria.getOperation().equalsIgnoreCase(">")) {
return criteriaBuilder.greaterThanOrEqualTo(root.<String>get(criteria.getKey()), criteria.getValue().toString());
} else if (criteria.getOperation().equalsIgnoreCase("<")) {
return criteriaBuilder.lessThanOrEqualTo(root.<String>get(criteria.getKey()), criteria.getValue().toString());
} else if (criteria.getOperation().equalsIgnoreCase(":")) {
if (criteria.getKey().contains(".")) {
String[] joinCriteriaArray = criteria.getKey().split("\\.");
Class<?> joinedClass = root.get(joinCriteriaArray[0]).getClass();
Join<T, ?> joinedRelationship = root.join(joinCriteriaArray[0]);
return criteriaBuilder.equal(joinedRelationship.get(joinCriteriaArray[1]), criteria.getValue());
}
if (root.get(criteria.getKey()).getJavaType() == String.class) {
return criteriaBuilder.like(root.<String>get(criteria.getKey()), "%" + criteria.getValue() + "%");
} else {
return criteriaBuilder.equal(root.get(criteria.getKey()), criteria.getValue());
}
}
return null;
}
}
Any Entity that I want to allow this type of Querying then just needs to have a concrete implementation of the AbstractEntitySpecification
public class PersonSpecification extends AbstractEntitySpecification<Person, Integer> {
public PersonSpecification (final EntitySearchCriteria entitySearchCriteria) {
this.criteria = entitySearchCriteria;
}
}
These are the tests that I have run. Any search on a attribute of Person that is a String or Int (i.e. Person.name, Person.age) will work, but a search on dateOfBirth will not.
Any search on an attribute of the pet that is a string will work using the join, but searching on the id(Integer) will not, no matter if I pass the id as an Int, or a String. I have put the behaviour in a comment for each test.
public class PersonSpecificationMediumTest extends AbstractMediumTest {
#Autowired
private PersonRepository personRepository;
#Autowired
private PetRepository petRepository;
Person person1;
Person person2;
#Before
public void setUp() {
Pet muffins = new Pet(1, "cat", "muffins", 4);
Pet rex= new Pet(2, "dog", "rex", 4);
petRepository.saveAll(Arrays.asList(muffins , rex));
person1 = new Person();
person1.setName("David");
person1.setDateOfBirth(Date.parse("1979-03-01");
person1.setPet(muffins);
person1 = personRepository.saveAndFlush(person1);
person2 = new Person();
person2.setName("Mary");
person2.setDateOfBirth(Date.parse("1982-03-01");
person2.setPet(rex);
person2 = personRepository.saveAndFlush(person2);
}
#Test //Works
public void toPredicate_findByNameEquals_assertCorrectResult() {
PersonSpecification spec
= new PersonSpecification(new EntitySearchCriteria("name", ":", "David"));
List<Person> results = personRepository.findAll(spec);
Assert.assertEquals(person1, results.get(0));
}
#Test // Works
public void toPredicate_findByPetNameEquals_assertCorrectResult() {
PersonSpecification spec
= new PersonSpecification(new EntitySearchCriteria("client.name", ":", "Rex"));
List<Person> results = personRepository.findAll(spec);
Assert.assertEquals(person2, results.get(0));
}
#Test // Return empty list. Cannot find the pet by Id.
public void toPredicate_findByPetIdEquals_assertCorrectResult() {
PersonSpecification spec
= new PersonSpecification(new EntitySearchCriteria("pet.id", ":", 2));
List<Person> results = personRepository.findAll(spec);
Assert.assertEquals(person2, results.get(0));
}
#Test // org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2] did not match expected type [java.lang.Integer (n/a)];
public void toPredicate_findByPetIdAsStringEquals_assertCorrectResult() {
PersonSpecification spec
= new PersonSpecification(new EntitySearchCriteria("pet.id", ":", "2"));
List<Person> results = personRepository.findAll(spec);
Assert.assertEquals(person2, results.get(0));
}
#Test // Fails on org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2020-01-01] did not match expected type [java.util.Date (n/a)]
public void toPredicate_findByDateOfBirthBetween_assertCorrectResult() {
PersonSpecification spec1
= new PersonSpecification(new EntitySearchCriteria("dateOfBirth", "<", "1990-01-01"));
PersonSpecification spec2
= new PersonSpecification(new EntitySearchCriteria("dateOfBirth", ">", "1970-01-01"));
List<Person> results = personRepository.findAll(spec1.and(spec2));
Assert.assertTrue(results.size() == 2);
}
}
Any idea why Date is so problematic? I wanted use the date in the greaterThanOrEqualTo and lessThanOrEqualTo, but passing in criteria.getValue(Object) gives a compile error so it forces me to use a string representation of the object. But the error shown is org.springframework.dao.InvalidDataAccessApiUsageException: Parameter value [2020-01-01] did not match expected type [java.util.Date (n/a)] which indicates to me that it cannot compare a String to a Date, which makes sense, but why stop me from passing the Date object?
Also, why is Id such an issue on the joined table? Why can it not find id = 2, I would have thought it would be straight forward, especially since I can search by the number of legs of the Pets successfully. It must have something to do with id being Serializable.
Check out JavaDoc for Date.parse. The essential part is already with the declaration:
#Deprecated
public static long parse(String s)
As it is clearly stated it returns a long value. To get a Date object you could use SimpleDateFormat that inherits DateFormat.parse(String s), like:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date d1 = sdf.parse("1979-03-01");
I have an enum:
public enum NotificationType {
OPEN("open"),
CLOSED("closed");
public String value;
NotificationType(String value) {
this.value = value;
}
}
I want to pass the custom string open or closed rather than OPEN or CLOSED to entity. Currently, I've mapped it in the entity as follows:
#Enumerated(EnumType.STRING)
private NotificationType notificationType;
Which is the best way to store/ fetch enum value?
You can create a custom converter like this:
#Converter(autoApply = true)
public class NotificationTypeConverter implements AttributeConverter<NotificationType, String> {
#Override
public String convertToDatabaseColumn(NotificationType notificationType) {
return notificationType == null
? null
: notificationType.value;
}
#Override
public NotificationType convertToEntityAttribute(String code) {
if (code == null || code.isEmpty()) {
return null;
}
return Arrays.stream(NotificationType.values())
.filter(c -> c.value.equals(code))
.findAny()
.orElseThrow(IllegalArgumentException::new);
}
}
And perhaps you'll need to remove annotation from your notificationType field so that this converter takes effect.
Yes, basically you have to develop a custom converter for that but I suggest you use Optional to avoid dealing with null and exceptions.
Add in NotificationType:
public static Optional<NotificationType> getFromValue(String value) {
return Optional.ofNullable(value)
.flatMap(dv -> Arrays.stream(NotificationType.values())
.filter(ev -> dv.equals(ev.value))
.findFirst());
}
Create the required converter:
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;
#Converter(autoApply = true)
public class NotificationTypeConverter implements AttributeConverter<NotificationType, String> {
#Override
public String convertToDatabaseColumn(NotificationType notificationType) {
return null == notificationType ? null : notificationType.value;
}
#Override
public NotificationType convertToEntityAttribute(String databaseValue) {
return NotificationType.getFromValue(databaseValue)
.orElse(null);
}
}
And now, you only have to modify your model:
#Entity
#Table
public class MyEntity {
#Convert(converter=NotificationTypeConverter.class)
private NotificationType notificationType;
}
I am trying to have a class that has a certain list of objects (specified by another class) persisted in the database as a string (use JPA Converter - all good).
And then I want to use Specification to search inside that string.
What is the best way to create the predicates? I don't seem to understand the connection bettween the AttributeConverter and the Expression in the Specification.
The parent class:
#Entity #Table
public class A {
#Column #Id #GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
#Column
private String name;
#Enumerated(EnumType.STRING)
private SomeType type;
#Column(length=1000) #Convert(converter = BConverter.class)
private List<B> bList;
private Integer no;
}
The listed object class:
public class B{
private String type;
private Integer quantity;
}
The converter:
#Converter
public class BConverter implements AttributeConverter<List<B>, String> {
private static final String SEPARATOR = ":";
private static final String LIST_SEPARATOR = ";";
#Override public String convertToDatabaseColumn(List<B> bList) {
return bList.stream().map(e->convertToString(e)).collect(Collectors.joining(LIST_SEPARATOR));
}
#Override public List<B> convertToEntityAttribute(String str) {
if(str==null || str.isEmpty() ) return null;
return Arrays.stream(str.split(LIST_SEPARATOR)).map(e->convertFromString(e)).collect(Collectors.toList());
}
private String convertToString(B b){
if(entity==null) return null;
return b.getType().toString() +SEPARATOR+ b.getQuantity().toString();
}
private B convertFromString(String subStr){
if(subStr==null || subStr.isEmpty() ) return null;
String[] pair = subStr.split(SEPARATOR);
return new B(pair[0],Integer.valueOf(pair[1]));
}
}
In the database should look something like:
Table A:
id: 1;
name: "Some Name";
type: "THISTYPE";
blist: "TYPE1:11;TYPE2:22";
no: 0;
id: 2;
name: "Other Name";
type: "THISTYPE";
blist: "TYPE1:45;TYPE2:56";
no: 12;
I would then like to create Specifications to search over this table for the attributes inside the bList.
For example, search by an entity that contains a B object where type=TYPE1 and a quantity>=30.
public static Specification<A> customSpecification(String type, Integer value) {
return (root, query, builder) -> ///?????????
}
Is there a way to use such specifications where the DB attribute is a String but JAVA only sees the objects?
My question is actually a spin-off of this question as seen here... so it might help to check that thread before proceeding.
In my Spring Boot project, I have two entities Sender and Recipient which represent a Customer and pretty much have the same fields, so I make them extend the base class Customer;
Customer base class;
#MappedSuperclass
public class Customer extends AuditableEntity {
#Column(name = "firstname")
private String firstname;
#Transient
private CustomerRole role;
public Customer(CustomerRole role) {
this.role = role;
}
//other fields & corresponding getters and setters
}
Sender domain object;
#Entity
#Table(name = "senders")
public class Sender extends Customer {
public Sender(){
super.setRole(CustomerRole.SENDER);
}
}
Recipient domain object;
#Entity
#Table(name = "recipients")
public class Recipient extends Customer {
public Recipient(){
super.setRole(CustomerRole.RECIPIENT);
}
}
NOTE - Sender and Recipient are exactly alike except for their roles. These can be easily stored in a single customers Table by making the Customer base class an entity itself, but I intentionally separate the entities this way because I have an obligation to persist each customer type in separate database tables.
Now I have one form in a view that collects details of both Sender & Recipient, so for example to collect the firstname, I had to name the form fields differently as follows;
Sender section of the form;
<input type="text" id="senderFirstname" name="senderFirstname" value="$!sender.firstname">
Recipient section of the form;
<input type="text" id="recipientFirstname" name="recipientFirstname" value="$!recipient.firstname">
But the fields available for a customer are so many that I'm looking for a way to map them to a pojo by means of an annotation as asked in this question here. However, the solutions provided there would mean that I have to create separate proxies for both domain objects and annotate the fields accordingly e.g
public class SenderProxy {
#ParamName("senderFirstname")
private String firstname;
#ParamName("senderLastname")
private String lastname;
//...
}
public class RecipientProxy {
#ParamName("recipientFirstname")
private String firstname;
#ParamName("recipientLastname")
private String lastname;
//...
}
So I got very curious and was wondering, is there a way to map this Proxies to more than one #ParamName such that the base class for example can just be annotated as follows?;
#MappedSuperclass
public class Customer extends AuditableEntity {
#Column(name = "firstname")
#ParamNames({"senderFirstname", "recipientFirstname"})
private String firstname;
#Column(name = "lastname")
#ParamNames({"senderLastname", "recipientLastname"})
private String lastname;
#Transient
private CustomerRole role;
public Customer(CustomerRole role) {
this.role = role;
}
//other fields & corresponding getters and setters
}
And then perhaps find a way to select value of fields based on annotation??
A suggestion from Zhang Jie like ExtendedBeanInfo
so i do it this way
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#Documented
public #interface Alias {
String[] value();
}
public class AliasedBeanInfoFactory implements BeanInfoFactory, Ordered {
#Override
public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
return supports(beanClass) ? new AliasedBeanInfo(Introspector.getBeanInfo(beanClass)) : null;
}
private boolean supports(Class<?> beanClass) {
Class<?> targetClass = beanClass;
do {
Field[] fields = targetClass.getDeclaredFields();
for (Field field : fields) {
if (field.isAnnotationPresent(Alias.class)) {
return true;
}
}
targetClass = targetClass.getSuperclass();
} while (targetClass != null && targetClass != Object.class);
return false;
}
#Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 100;
}
}
public class AliasedBeanInfo implements BeanInfo {
private static final Logger LOGGER = LoggerFactory.getLogger(AliasedBeanInfo.class);
private final BeanInfo delegate;
private final Set<PropertyDescriptor> propertyDescriptors = new TreeSet<>(new PropertyDescriptorComparator());
AliasedBeanInfo(BeanInfo delegate) {
this.delegate = delegate;
this.propertyDescriptors.addAll(Arrays.asList(delegate.getPropertyDescriptors()));
Class<?> beanClass = delegate.getBeanDescriptor().getBeanClass();
for (Field field : findAliasedFields(beanClass)) {
Optional<PropertyDescriptor> optional = findExistingPropertyDescriptor(field.getName(), field.getType());
if (!optional.isPresent()) {
LOGGER.warn("there is no PropertyDescriptor for field[{}]", field);
continue;
}
Alias alias = field.getAnnotation(Alias.class);
addAliasPropertyDescriptor(alias.value(), optional.get());
}
}
private List<Field> findAliasedFields(Class<?> beanClass) {
List<Field> fields = new ArrayList<>();
ReflectionUtils.doWithFields(beanClass,
fields::add,
field -> field.isAnnotationPresent(Alias.class));
return fields;
}
private Optional<PropertyDescriptor> findExistingPropertyDescriptor(String propertyName, Class<?> propertyType) {
return propertyDescriptors
.stream()
.filter(pd -> pd.getName().equals(propertyName) && pd.getPropertyType().equals(propertyType))
.findAny();
}
private void addAliasPropertyDescriptor(String[] values, PropertyDescriptor propertyDescriptor) {
for (String value : values) {
if (!value.isEmpty()) {
try {
this.propertyDescriptors.add(new PropertyDescriptor(
value, propertyDescriptor.getReadMethod(), propertyDescriptor.getWriteMethod()));
} catch (IntrospectionException e) {
LOGGER.error("add field[{}] alias[{}] property descriptor error", propertyDescriptor.getName(),
value, e);
}
}
}
}
#Override
public BeanDescriptor getBeanDescriptor() {
return this.delegate.getBeanDescriptor();
}
#Override
public EventSetDescriptor[] getEventSetDescriptors() {
return this.delegate.getEventSetDescriptors();
}
#Override
public int getDefaultEventIndex() {
return this.delegate.getDefaultEventIndex();
}
#Override
public PropertyDescriptor[] getPropertyDescriptors() {
return this.propertyDescriptors.toArray(new PropertyDescriptor[0]);
}
#Override
public int getDefaultPropertyIndex() {
return this.delegate.getDefaultPropertyIndex();
}
#Override
public MethodDescriptor[] getMethodDescriptors() {
return this.delegate.getMethodDescriptors();
}
#Override
public BeanInfo[] getAdditionalBeanInfo() {
return this.delegate.getAdditionalBeanInfo();
}
#Override
public Image getIcon(int iconKind) {
return this.delegate.getIcon(iconKind);
}
static class PropertyDescriptorComparator implements Comparator<PropertyDescriptor> {
#Override
public int compare(PropertyDescriptor desc1, PropertyDescriptor desc2) {
String left = desc1.getName();
String right = desc2.getName();
for (int i = 0; i < left.length(); i++) {
if (right.length() == i) {
return 1;
}
int result = left.getBytes()[i] - right.getBytes()[i];
if (result != 0) {
return result;
}
}
return left.length() - right.length();
}
}
}