View Javadoc
1   package ca.uhn.fhir.jpa.util;
2   
3   /*-
4    * #%L
5    * HAPI FHIR JPA Server
6    * %%
7    * Copyright (C) 2014 - 2019 University Health Network
8    * %%
9    * Licensed under the Apache License, Version 2.0 (the "License");
10   * you may not use this file except in compliance with the License.
11   * You may obtain a copy of the License at
12   *
13   *      http://www.apache.org/licenses/LICENSE-2.0
14   *
15   * Unless required by applicable law or agreed to in writing, software
16   * distributed under the License is distributed on an "AS IS" BASIS,
17   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18   * See the License for the specific language governing permissions and
19   * limitations under the License.
20   * #L%
21   */
22  
23  import ca.uhn.fhir.rest.api.Constants;
24  import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
25  import com.google.common.collect.ImmutableSet;
26  import com.google.common.collect.Lists;
27  import com.google.common.reflect.ClassPath;
28  import com.google.common.reflect.ClassPath.ClassInfo;
29  import org.apache.commons.io.IOUtils;
30  import org.apache.commons.lang3.Validate;
31  import org.hibernate.validator.constraints.Length;
32  import org.hl7.fhir.instance.model.api.IBaseResource;
33  import org.hl7.fhir.r4.model.InstantType;
34  
35  import javax.persistence.*;
36  import javax.validation.constraints.Size;
37  import java.io.IOException;
38  import java.io.InputStream;
39  import java.lang.reflect.AnnotatedElement;
40  import java.lang.reflect.Field;
41  import java.lang.reflect.Modifier;
42  import java.util.*;
43  import java.util.stream.Collectors;
44  
45  import static com.google.common.base.Ascii.toUpperCase;
46  import static org.apache.commons.lang3.StringUtils.isBlank;
47  import static org.apache.commons.lang3.StringUtils.isNotBlank;
48  
49  public class TestUtil {
50  	public static final int MAX_COL_LENGTH = 2000;
51  	private static final int MAX_LENGTH = 30;
52  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(TestUtil.class);
53  	private static Set<String> ourReservedWords;
54  
55  	/**
56  	 * non instantiable
57  	 */
58  	private TestUtil() {
59  		super();
60  	}
61  
62  	/**
63  	 * This is really only useful for unit tests, do not call otherwise
64  	 */
65  	@SuppressWarnings("UnstableApiUsage")
66  	public static void scanEntities(String packageName) throws IOException, ClassNotFoundException {
67  
68  		try (InputStream is = TestUtil.class.getResourceAsStream("/mysql-reserved-words.txt")) {
69  			String contents = IOUtils.toString(is, Constants.CHARSET_UTF8);
70  			String[] words = contents.split("\\n");
71  			ourReservedWords = Arrays.stream(words)
72  				.filter(t -> isNotBlank(t))
73  				.map(t -> toUpperCase(t))
74  				.collect(Collectors.toSet());
75  		}
76  
77  		ImmutableSet<ClassInfo> classes = ClassPath.from(TestUtil.class.getClassLoader()).getTopLevelClasses(packageName);
78  		Set<String> names = new HashSet<String>();
79  
80  		if (classes.size() <= 1) {
81  			throw new InternalErrorException("Found no classes");
82  		}
83  
84  		for (ClassInfo classInfo : classes) {
85  			Class<?> clazz = Class.forName(classInfo.getName());
86  			Entity entity = clazz.getAnnotation(Entity.class);
87  			if (entity == null) {
88  				continue;
89  			}
90  
91  			scanClass(names, clazz, false);
92  
93  		}
94  	}
95  
96  	private static void scanClass(Set<String> theNames, Class<?> theClazz, boolean theIsSuperClass) {
97  		ourLog.info("Scanning: {}", theClazz.getSimpleName());
98  
99  		scan(theClazz, theNames, theIsSuperClass);
100 
101 		for (Field nextField : theClazz.getDeclaredFields()) {
102 			if (Modifier.isStatic(nextField.getModifiers())) {
103 				continue;
104 			}
105 
106 			ourLog.info(" * Scanning field: {}", nextField.getName());
107 			scan(nextField, theNames, theIsSuperClass);
108 
109 			Lob lobClass = nextField.getAnnotation(Lob.class);
110 			if (lobClass != null) {
111 				if (nextField.getType().equals(byte[].class) == false) {
112 					//Validate.isTrue(false);
113 				}
114 			}
115 
116 			boolean isTransient = nextField.getAnnotation(Transient.class) != null;
117 			if (!isTransient) {
118 				boolean hasColumn = nextField.getAnnotation(Column.class) != null;
119 				boolean hasJoinColumn = nextField.getAnnotation(JoinColumn.class) != null;
120 				boolean hasEmbeddedId = nextField.getAnnotation(EmbeddedId.class) != null;
121 				OneToMany oneToMany = nextField.getAnnotation(OneToMany.class);
122 				OneToOne oneToOne = nextField.getAnnotation(OneToOne.class);
123 				boolean isOtherSideOfOneToManyMapping = oneToMany != null && isNotBlank(oneToMany.mappedBy());
124 				boolean isOtherSideOfOneToOneMapping = oneToOne != null && isNotBlank(oneToOne.mappedBy());
125 				Validate.isTrue(
126 					hasColumn ||
127 						hasJoinColumn ||
128 						isOtherSideOfOneToManyMapping ||
129 						isOtherSideOfOneToOneMapping ||
130 						hasEmbeddedId, "Non-transient has no @Column or @JoinColumn or @EmbeddedId: " + nextField);
131 			}
132 
133 
134 		}
135 
136 		if (theClazz.getSuperclass().equals(Object.class)) {
137 			return;
138 		}
139 
140 		scanClass(theNames, theClazz.getSuperclass(), true);
141 	}
142 
143 	private static void scan(AnnotatedElement theAnnotatedElement, Set<String> theNames, boolean theIsSuperClass) {
144 		Table table = theAnnotatedElement.getAnnotation(Table.class);
145 		if (table != null) {
146 
147 			// Banned name because we already used it once
148 			ArrayList<String> bannedNames = Lists.newArrayList("CDR_USER_2FA", "TRM_VALUESET_CODE");
149 			Validate.isTrue(!bannedNames.contains(table.name().toUpperCase()));
150 
151 			Validate.isTrue(table.name().toUpperCase().equals(table.name()));
152 
153 			assertNotADuplicateName(table.name(), theNames);
154 			for (UniqueConstraint nextConstraint : table.uniqueConstraints()) {
155 				assertNotADuplicateName(nextConstraint.name(), theNames);
156 				Validate.isTrue(nextConstraint.name().startsWith("IDX_"), nextConstraint.name() + " must start with IDX_");
157 			}
158 			for (Index nextConstraint : table.indexes()) {
159 				assertNotADuplicateName(nextConstraint.name(), theNames);
160 				Validate.isTrue(nextConstraint.name().startsWith("IDX_"), nextConstraint.name() + " must start with IDX_");
161 			}
162 		}
163 
164 		JoinColumn joinColumn = theAnnotatedElement.getAnnotation(JoinColumn.class);
165 		if (joinColumn != null) {
166 			String columnName = joinColumn.name();
167 			validateColumnName(columnName, theAnnotatedElement);
168 
169 			assertNotADuplicateName(columnName, null);
170 			ForeignKey fk = joinColumn.foreignKey();
171 			if (theIsSuperClass) {
172 				Validate.isTrue(isBlank(fk.name()), "Foreign key on " + theAnnotatedElement.toString() + " has a name() and should not as it is a superclass");
173 			} else {
174 				Validate.notNull(fk);
175 				Validate.isTrue(isNotBlank(fk.name()), "Foreign key on " + theAnnotatedElement.toString() + " has no name()");
176 				Validate.isTrue(fk.name().startsWith("FK_"));
177 				assertNotADuplicateName(fk.name(), theNames);
178 			}
179 		}
180 
181 		Column column = theAnnotatedElement.getAnnotation(Column.class);
182 		if (column != null) {
183 			String columnName = column.name();
184 			validateColumnName(columnName, theAnnotatedElement);
185 
186 			assertNotADuplicateName(columnName, null);
187 			Validate.isTrue(column.unique() == false, "Should not use unique attribute on column (use named @UniqueConstraint instead) on " + theAnnotatedElement.toString());
188 
189 			boolean hasLob = theAnnotatedElement.getAnnotation(Lob.class) != null;
190 			Field field = (Field) theAnnotatedElement;
191 
192 			/*
193 			 * For string columns, we want to make sure that an explicit max
194 			 * length is always specified, and that this max is always sensible.
195 			 * Unfortunately there is no way to differentiate between "explicitly
196 			 * set to 255" and "just using the default of 255" so we have banned
197 			 * the exact length of 255.
198 			 */
199 			if (field.getType().equals(String.class)) {
200 				if (!hasLob) {
201 					if (column.length() == 255) {
202 						throw new IllegalStateException("Field does not have an explicit maximum length specified: " + field);
203 					}
204 					if (column.length() > MAX_COL_LENGTH) {
205 						throw new IllegalStateException("Field is too long: " + field);
206 					}
207 				}
208 
209 				Size size = theAnnotatedElement.getAnnotation(Size.class);
210 				if (size != null) {
211 					if (size.max() > MAX_COL_LENGTH) {
212 						throw new IllegalStateException("Field is too long: " + field);
213 					}
214 				}
215 
216 				Length length = theAnnotatedElement.getAnnotation(Length.class);
217 				if (length != null) {
218 					if (length.max() > MAX_COL_LENGTH) {
219 						throw new IllegalStateException("Field is too long: " + field);
220 					}
221 				}
222 			}
223 
224 		}
225 
226 		GeneratedValue gen = theAnnotatedElement.getAnnotation(GeneratedValue.class);
227 		SequenceGenerator sg = theAnnotatedElement.getAnnotation(SequenceGenerator.class);
228 		Validate.isTrue((gen != null) == (sg != null));
229 		if (gen != null) {
230 			assertNotADuplicateName(gen.generator(), theNames);
231 			assertNotADuplicateName(sg.name(), null);
232 			assertNotADuplicateName(sg.sequenceName(), null);
233 			assertEquals(gen.generator(), sg.name());
234 			assertEquals(gen.generator(), sg.sequenceName());
235 		}
236 
237 	}
238 
239 	private static void validateColumnName(String theColumnName, AnnotatedElement theElement) {
240 		if (!theColumnName.equals(theColumnName.toUpperCase())) {
241 			throw new IllegalArgumentException("Column name must be all upper case: " + theColumnName + " found on " + theElement);
242 		}
243 		if (ourReservedWords.contains(theColumnName)) {
244 			throw new IllegalArgumentException("Column name is a reserved word: " + theColumnName + " found on " + theElement);
245 		}
246 	}
247 
248 	private static void assertEquals(String theGenerator, String theName) {
249 		Validate.isTrue(theGenerator.equals(theName));
250 	}
251 
252 	private static void assertNotADuplicateName(String theName, Set<String> theNames) {
253 		if (isBlank(theName)) {
254 			return;
255 		}
256 		Validate.isTrue(theName.length() <= MAX_LENGTH, "Identifier \"" + theName + "\" is " + theName.length() + " chars long");
257 		if (theNames != null) {
258 			Validate.isTrue(theNames.add(theName), "Duplicate name: " + theName);
259 		}
260 	}
261 
262 	public static void sleepAtLeast(int theMillis) {
263 		long start = System.currentTimeMillis();
264 		while (System.currentTimeMillis() <= start + theMillis) {
265 			try {
266 				long timeSinceStarted = System.currentTimeMillis() - start;
267 				long timeToSleep = Math.max(0, theMillis - timeSinceStarted);
268 				ourLog.info("Sleeping for {}ms", timeToSleep);
269 				Thread.sleep(timeToSleep);
270 			} catch (InterruptedException theE) {
271 				ourLog.error("Interrupted", theE);
272 			}
273 		}
274 	}
275 
276 
277 	public static void clearAllStaticFieldsForUnitTest() {
278 		ca.uhn.fhir.util.TestUtil.clearAllStaticFieldsForUnitTest();
279 	}
280 
281 	public static InstantType getTimestamp(IBaseResource resource) {
282 		return new InstantType(new Date(resource.getMeta().getLastUpdated().getTime()));
283 	}
284 
285 	public static void sleepOneClick() {
286 		sleepAtLeast(1);
287 	}
288 
289 
290 }