Skip to content

Commit cecef12

Browse files
authored
Merge pull request #435 from scijava/pref-service-arrays-fix
Improve array -> `String` conversions from `DefaultConverter`
2 parents aa7fe2e + 1df50dd commit cecef12

File tree

5 files changed

+726
-2
lines changed

5 files changed

+726
-2
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* #%L
3+
* SciJava Common shared library for SciJava software.
4+
* %%
5+
* Copyright (C) 2009 - 2022 SciJava developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.scijava.convert;
31+
32+
import java.lang.reflect.Array;
33+
import java.lang.reflect.Type;
34+
import java.util.stream.Collectors;
35+
36+
import org.scijava.Priority;
37+
import org.scijava.plugin.Parameter;
38+
import org.scijava.plugin.Plugin;
39+
import org.scijava.util.ArrayUtils;
40+
import org.scijava.util.Types;
41+
42+
/**
43+
* A {@link Converter} that specializes in converting
44+
* n-dimensional arrays into {@link String}s. This {@link Converter} can convert any array whose
45+
* component types can be converted into {@link String}s. By default, this
46+
* {@link Converter} delimits the array elements with commas.
47+
*
48+
* @author Gabriel Selzer
49+
*/
50+
@Plugin(type = Converter.class, priority = Priority.VERY_LOW)
51+
public class ArrayToStringConverter extends AbstractConverter<Object, String> {
52+
53+
@Parameter private ConvertService convertService;
54+
55+
@Override public boolean canConvert(final Class<?> src, final Class<?> dest) {
56+
if (src == null) return false;
57+
final Class<?> saneSrc = Types.box(src);
58+
final Class<?> saneDest = Types.box(dest);
59+
return saneSrc.isArray() && saneDest == String.class;
60+
}
61+
62+
@Override public boolean canConvert(final Object src, final Class<?> dest) {
63+
if (!canConvert(src.getClass(), dest)) return false;
64+
if (Array.getLength(src) == 0) return true;
65+
return convertService.supports(Array.get(src, 0), dest);
66+
}
67+
68+
@Override
69+
public Object convert(Object src, Type dest) {
70+
// Preprocess the "string-likes"
71+
if (src.getClass().equals(String[].class) || //
72+
src.getClass().equals(Character[].class) || //
73+
src.getClass().equals(char[].class)) //
74+
{
75+
src = preprocessCharacters(src);
76+
}
77+
// Convert each element to Strings
78+
String elementString = ArrayUtils.toCollection(src).stream() //
79+
.map(object -> convertService.convert(object, String.class)) //
80+
.collect(Collectors.joining(", "));
81+
return "{" + elementString + "}";
82+
}
83+
84+
private String[] preprocessStrings(final Object src) {
85+
int numElements = Array.getLength(src);
86+
String[] processed = new String[numElements];
87+
for (int i = 0; i < numElements; i++) {
88+
processed[i] = preprocessString(Array.get(src, i));
89+
}
90+
return processed;
91+
}
92+
93+
private String preprocessString(final Object o) {
94+
String s = o.toString();
95+
s = s.replace("\\", "\\\\");
96+
s = s.replace("\"", "\\\"");
97+
return "\"" + s + "\"";
98+
}
99+
100+
private String[] preprocessCharacters(Object src) {
101+
String[] processed = new String[Array.getLength(src)];
102+
for (int i = 0; i < processed.length; i++) {
103+
processed[i] = Array.get(src, i).toString();
104+
}
105+
return preprocessStrings(processed);
106+
}
107+
108+
@SuppressWarnings("unchecked") @Override
109+
public <T> T convert(Object src, Class<T> dest) {
110+
return (T) convert(src, (Type) dest);
111+
}
112+
113+
@Override public Class<String> getOutputType() {
114+
return String.class;
115+
}
116+
117+
@Override public Class<Object> getInputType() {
118+
return Object.class;
119+
}
120+
121+
}
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/*
2+
* #%L
3+
* SciJava Common shared library for SciJava software.
4+
* %%
5+
* Copyright (C) 2009 - 2022 SciJava developers.
6+
* %%
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19+
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
20+
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21+
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
* #L%
28+
*/
29+
30+
package org.scijava.convert;
31+
32+
import org.scijava.Priority;
33+
import org.scijava.parse.Items;
34+
import org.scijava.parse.ParseService;
35+
import org.scijava.parsington.ExpressionParser;
36+
import org.scijava.parsington.SyntaxTree;
37+
import org.scijava.plugin.Parameter;
38+
import org.scijava.plugin.Plugin;
39+
import org.scijava.util.Types;
40+
41+
import java.lang.reflect.Array;
42+
import java.lang.reflect.Type;
43+
44+
/**
45+
* A {@link Converter} that specializes in converting {@link String}s to
46+
* n-dimensional arrays. This {@link Converter} can convert any array whose
47+
* component types can be created from a {@link String}. By default, this
48+
* {@link Converter} delimits the {@link String} based on commas.
49+
*
50+
* @author Gabriel Selzer
51+
*/
52+
@Plugin(type = Converter.class, priority = Priority.VERY_LOW)
53+
public class StringToArrayConverter extends AbstractConverter<String, Object> {
54+
55+
@Parameter
56+
private ConvertService convertService;
57+
58+
@Parameter
59+
private ParseService parseService;
60+
61+
private final ExpressionParser parser = new ExpressionParser();
62+
63+
@Override
64+
public boolean canConvert(final Class<?> src, final Class<?> dest) {
65+
if (src == null) return false;
66+
final Class<?> saneSrc = Types.box(src);
67+
final Class<?> saneDest = Types.box(dest);
68+
return saneSrc == String.class && saneDest.isArray();
69+
}
70+
71+
@Override
72+
public boolean canConvert(final Object src, final Type dest) {
73+
return canConvert(src, Types.raw(dest));
74+
}
75+
76+
@Override
77+
public boolean canConvert(final Object src, final Class<?> dest) {
78+
79+
// First, ensure the base types conform
80+
if (!canConvert(src.getClass(), dest)) return false;
81+
// Then, ensure we can parse the string
82+
SyntaxTree tree;
83+
try {
84+
tree = parser.parseTree((String) src);
85+
}
86+
catch (IllegalArgumentException e) {
87+
return false;
88+
}
89+
// We can always convert empty arrays as we don't have to create Objects
90+
if (tree.count() == 0) return true;
91+
// Finally, ensure that we can convert the elements of the array.
92+
// NB this check is merely a heuristic. In the case of a heterogeneous
93+
// array, canConvert may falsely return positive, if later elements in the
94+
// string-ified array cannot be converted into Objects. We make this
95+
// compromise in the interest of speed, however, as ensuring correctness
96+
// would require a premature conversion of the entire array.
97+
Object testSrc = firstElement(tree);
98+
Class<?> testDest = unitComponentType(dest);
99+
return convertService.supports(testSrc, testDest);
100+
}
101+
102+
@Override
103+
public Object convert(Object src, Type dest) {
104+
final Type componentType = Types.component(dest);
105+
if (componentType == null) {
106+
throw new IllegalArgumentException(dest + " is not an array type!");
107+
}
108+
try {
109+
return convertToArray( //
110+
parseService.parse((String) src), //
111+
Types.raw(componentType));
112+
}
113+
catch (IllegalArgumentException e) {
114+
return null;
115+
}
116+
}
117+
118+
@SuppressWarnings("unchecked")
119+
@Override
120+
public <T> T convert(Object src, Class<T> dest) {
121+
return (T) convert(src, (Type) dest);
122+
}
123+
124+
@Override
125+
public Class<Object> getOutputType() {
126+
return Object.class;
127+
}
128+
129+
@Override
130+
public Class<String> getInputType() {
131+
return String.class;
132+
}
133+
134+
// -- HELPER METHODS -- //
135+
136+
/**
137+
* Converts {@code src} into an array of component type {@code componentType}
138+
*
139+
* @param tree the {@link String} to convert
140+
* @param componentType the component type of the output array
141+
* @return an array of {@code componentType} whose elements were created from
142+
* {@code src}
143+
*/
144+
private Object convertToArray(Items tree, final Class<?> componentType) {
145+
// Create the array
146+
final Object array = Array.newInstance(componentType, tree.size());
147+
// Set each element of the array
148+
for (int i = 0; i < tree.size(); i++) {
149+
Object element = convertService.convert(tree.get(i).value(), componentType);
150+
Array.set(array, i, element);
151+
}
152+
return array;
153+
}
154+
155+
/**
156+
* Similar to {@link Class#getComponentType()}, but handles nested array types
157+
*
158+
* @param c the {@link Class} that may be an array class
159+
* @return the <em>unit</em> component type of {@link Class} {@code c}
160+
*/
161+
private Class<?> unitComponentType(Class<?> c) {
162+
if (!c.isArray()) return c;
163+
return unitComponentType(c.getComponentType());
164+
}
165+
166+
/**
167+
* Traverses {@code tree} to find the first element
168+
*
169+
* @param tree the {@link SyntaxTree} containing elements
170+
* @return the first {@link Object} in {@code tree}
171+
*/
172+
private Object firstElement(SyntaxTree tree) {
173+
while (tree.count() > 0) {
174+
tree = tree.child(0);
175+
}
176+
return tree.token();
177+
}
178+
179+
}

0 commit comments

Comments
 (0)