##### Coverage Report - org.jbehave.core.reporters.CrossReference

Classes in this File | Line Coverage | Branch Coverage | Complexity
CrossReference |
86% | 73/84
83% | 5/6
| 1.8333333333333333;1.833
CrossReference$1 |
74% | 20/27
37% | 3/8
| 1.8333333333333333;1.833
CrossReference$StepMatch |
35% | 7/20
0% | 0/18
| 1.8333333333333333;1.833
CrossReference$StepUsage |
100% | 5/5
N/A | N/A
| 1.8333333333333333;1.833
CrossReference$StoryHolder |
100% | 5/5
N/A | N/A
| 1.8333333333333333;1.833
CrossReference$XRefRoot |
97% | 39/40
62% | 10/16
| 1.8333333333333333;1.833
CrossReference$XRefStepMonitor |
75% | 12/16
50% | 3/6
| 1.8333333333333333;1.833
CrossReference$XRefStory |
82% | 28/34
60% | 6/10
| 1.8333333333333333;1.833
CrossReference$XrefOutputFailed |
0% | 0/2
N/A | N/A
| 1.8333333333333333;1.833

1 |  |
     package org.jbehave.core.reporters;
2 |  |
3 |  |
     import com.thoughtworks.xstream.XStream;
4 |  |
     import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
5 |  |
6 |  |
     import org.jbehave.core.failures.FailingUponPendingStep;
7 |  |
     import org.jbehave.core.failures.PassingUponPendingStep;
8 |  |
     import org.jbehave.core.failures.PendingStepStrategy;
9 |  |
     import org.jbehave.core.io.StoryLocation;
10 |  |
     import org.jbehave.core.model.ExamplesTable;
11 |  |
     import org.jbehave.core.model.Meta;
12 |  |
     import org.jbehave.core.model.Narrative;
13 |  |
     import org.jbehave.core.model.Scenario;
14 |  |
     import org.jbehave.core.model.StepPattern;
15 |  |
     import org.jbehave.core.model.Story;
16 |  |
     import org.jbehave.core.steps.NullStepMonitor;
17 |  |
     import org.jbehave.core.steps.StepMonitor;
18 |  |
     import org.jbehave.core.steps.StepType;
19 |  |
20 |  |
     import java.io.File;
21 |  |
     import java.io.FileWriter;
22 |  |
     import java.io.IOException;
23 |  |
     import java.io.Writer;
24 |  |
     import java.lang.reflect.Method;
25 |  |
     import java.util.ArrayList;
26 |  |
     import java.util.HashMap;
27 |  |
     import java.util.HashSet;
28 |  |
     import java.util.List;
29 |  |
     import java.util.Map;
30 |  |
     import java.util.Set;
31 |  |
32 | 72 |
     public class CrossReference extends Format {
33 |  |
34 | 5 |
         private final XStream XSTREAM_FOR_XML = new XStream();
35 | 5 |
         private final XStream XSTREAM_FOR_JSON = new XStream(new JsonHierarchicalStreamDriver());
36 | 5 |
         private ThreadLocal<Story> currentStory = new ThreadLocal<Story>();
37 | 5 |
         private ThreadLocal<Long> currentStoryStart = new ThreadLocal<Long>();
38 | 5 |
         private ThreadLocal<String> currentScenarioTitle = new ThreadLocal<String>();
39 | 5 |
         private List<StoryHolder> stories = new ArrayList<StoryHolder>();
40 | 5 |
         private Map<String, Long> times = new HashMap<String, Long>();
41 | 5 |
         private Map<String, StepMatch> stepMatches = new HashMap<String, StepMatch>();
42 | 5 |
         private StepMonitor stepMonitor = new XRefStepMonitor();
43 | 5 |
         private Set<String> failingStories = new HashSet<String>();
44 | 5 |
         private Set<String> stepsPerformed = new HashSet<String>();
45 | 5 |
         private PendingStepStrategy pendingStepStrategy = new PassingUponPendingStep();
46 | 5 |
         private String metaFilter = "";
47 | 5 |
         private boolean doJson = true;
48 | 5 |
         private boolean doXml = true;
49 | 5 |
         private boolean excludeStoriesWithNoExecutedScenarios = false;
50 | 5 |
         private boolean outputAfterEachStory = false;
51 |  |
         private Format threadSafeDelegateFormat;
52 |  |
53 |  |
         public CrossReference() {
54 | 5 |
             this("XREF");
55 | 5 |
         }
56 |  |
57 |  |
         public CrossReference(String name) {
58 | 5 |
             super(name);
59 | 5 |
             configure(XSTREAM_FOR_XML);
60 | 5 |
             configure(XSTREAM_FOR_JSON);
61 | 5 |
         }
62 |  |
63 |  |
         public CrossReference withJsonOnly() {
64 | 1 |
             doJson = true;
65 | 1 |
             doXml = false;
66 | 1 |
             return this;
67 |  |
         }
68 |  |
69 |  |
         public CrossReference withXmlOnly() {
70 | 1 |
             doJson = false;
71 | 1 |
             doXml = true;
72 | 1 |
             return this;
73 |  |
         }
74 |  |
75 |  |
         public CrossReference withMetaFilter(String metaFilter) {
76 | 0 |
             this.metaFilter = metaFilter;
77 | 0 |
             return this;
78 |  |
         }
79 |  |
80 |  |
         public CrossReference withPendingStepStrategy(PendingStepStrategy pendingStepStrategy) {
81 | 0 |
             this.pendingStepStrategy = pendingStepStrategy;
82 | 0 |
             return this;
83 |  |
         }
84 |  |
85 |  |
         public CrossReference withOutputAfterEachStory(boolean outputAfterEachStory) {
86 | 0 |
             this.outputAfterEachStory = outputAfterEachStory;
87 | 0 |
             return this;
88 |  |
         }
89 |  |
90 |  |
         public CrossReference withThreadSafeDelegateFormat(Format format) {
91 | 0 |
             this.threadSafeDelegateFormat = format;
92 | 0 |
             return this;
93 |  |
         }
94 |  |
95 |  |
         public CrossReference excludingStoriesWithNoExecutedScenarios(boolean exclude) {
96 | 4 |
             this.excludeStoriesWithNoExecutedScenarios = exclude;
97 | 4 |
             return this;
98 |  |
         }
99 |  |
100 |  |
         public String getMetaFilter() {
101 | 2 |
             return metaFilter;
102 |  |
         }
103 |  |
104 |  |
         public StepMonitor getStepMonitor() {
105 | 4 |
             return stepMonitor;
106 |  |
         }
107 |  |
108 |  |
         /**
109 |  |
          * Output to JSON and/or XML files. Could be at the end of the suite, or per
110 |  |
          * story In the case of the latter, synchronization is needed as two stories
111 |  |
          * (on two threads) could be completing concurrently, and we need to guard
112 |  |
          * against ConcurrentModificationException
113 |  |
          *
114 |  |
          * @param storyReporterBuilder the reporter to use
115 |  |
          */
116 |  |
         public synchronized void outputToFiles(StoryReporterBuilder storyReporterBuilder) {
117 | 5 |
             XRefRoot root = createXRefRoot(storyReporterBuilder, stories, failingStories);
118 | 5 |
             root.addStepMatches(stepMatches);
119 | 5 |
             if (doXml) {
120 | 4 |
                 outputFile(fileName("xml"), XSTREAM_FOR_XML, root, storyReporterBuilder);
121 |  |
             }
122 | 5 |
             if (doJson) {
123 | 4 |
                 outputFile(fileName("json"), XSTREAM_FOR_JSON, root, storyReporterBuilder);
124 |  |
             }
125 | 5 |
         }
126 |  |
127 |  |
         protected String fileName(String extension) {
128 | 8 |
             return name().toLowerCase() + "." + extension;
129 |  |
         }
130 |  |
131 |  |
         protected final XRefRoot createXRefRoot(StoryReporterBuilder storyReporterBuilder, List<StoryHolder> stories,
132 |  |
                 Set<String> failingStories) {
133 | 5 |
             XRefRoot xrefRoot = newXRefRoot();
134 | 5 |
             xrefRoot.metaFilter = getMetaFilter();
135 | 5 |
             xrefRoot.setExcludeStoriesWithNoExecutedScenarios(excludeStoriesWithNoExecutedScenarios);
136 | 5 |
             xrefRoot.processStories(stories, stepsPerformed, times, storyReporterBuilder, failingStories);
137 | 5 |
             return xrefRoot;
138 |  |
         }
139 |  |
140 |  |
         protected XRefRoot newXRefRoot() {
141 | 1 |
             return new XRefRoot();
142 |  |
         }
143 |  |
144 |  |
         private void outputFile(String name, XStream xstream, XRefRoot root, StoryReporterBuilder storyReporterBuilder) {
145 |  |
146 | 8 |
             File outputDir = new File(storyReporterBuilder.outputDirectory(), "view");
147 | 8 |
             outputDir.mkdirs();
148 |  |
             try {
149 | 8 |
                 Writer writer = makeWriter(new File(outputDir, name));
150 | 8 |
                 writer.write(xstream.toXML(root));
151 | 8 |
                 writer.flush();
152 | 8 |
                 writer.close();
153 | 0 |
             } catch (IOException e) {
154 | 0 |
                 throw new XrefOutputFailed(name, e);
155 | 8 |
             }
156 |  |
157 | 8 |
         }
158 |  |
159 |  |
         @SuppressWarnings("serial")
160 |  |
         public static class XrefOutputFailed extends RuntimeException {
161 |  |
162 |  |
             public XrefOutputFailed(String name, Throwable cause) {
163 | 0 |
                 super(name, cause);
164 | 0 |
             }
165 |  |
166 |  |
         }
167 |  |
168 |  |
         protected Writer makeWriter(File file) throws IOException {
169 | 2 |
             return new FileWriter(file);
170 |  |
         }
171 |  |
172 |  |
         private void configure(XStream xstream) {
173 | 10 |
             xstream.setMode(XStream.NO_REFERENCES);
174 | 10 |
             aliasForXRefRoot(xstream);
175 | 10 |
             aliasForXRefStory(xstream);
176 | 10 |
             xstream.alias("stepMatch", StepMatch.class);
177 | 10 |
             xstream.alias("pattern", StepPattern.class);
178 | 10 |
             xstream.alias("use", StepUsage.class);
179 | 10 |
             xstream.omitField(ExamplesTable.class, "parameterConverters");
180 | 10 |
             xstream.omitField(ExamplesTable.class, "defaults");
181 | 10 |
         }
182 |  |
183 |  |
         protected void aliasForXRefStory(XStream xstream) {
184 | 8 |
             xstream.alias("story", XRefStory.class);
185 | 8 |
         }
186 |  |
187 |  |
         protected void aliasForXRefRoot(XStream xstream) {
188 | 2 |
             xstream.alias("xref", XRefRoot.class);
189 | 2 |
         }
190 |  |
191 |  |
         @Override
192 |  |
         public StoryReporter createStoryReporter(FilePrintStreamFactory factory,
193 |  |
                 final StoryReporterBuilder storyReporterBuilder) {
194 |  |
             StoryReporter delegate;
195 | 4 |
             if (threadSafeDelegateFormat == null) {
196 | 4 |
                 delegate = new NullStoryReporter();
197 |  |
             } else {
198 | 0 |
                 delegate = threadSafeDelegateFormat.createStoryReporter(factory, storyReporterBuilder);
199 |  |
             }
200 | 4 |
             return new DelegatingStoryReporter(delegate) {
201 |  |
202 |  |
                 @Override
203 |  |
                 public void beforeStory(Story story, boolean givenStory) {
204 | 4 |
                     if (givenStory)
205 | 0 |
                         return;
206 | 4 |
                     synchronized (stories) {
207 | 4 |
                         stories.add(new StoryHolder(story));
208 | 4 |
                     }
209 | 4 |
                     currentStory.set(story);
210 | 4 |
                     currentStoryStart.set(System.currentTimeMillis());
211 | 4 |
                     super.beforeStory(story, givenStory);
212 | 4 |
                 }
213 |  |
214 |  |
                 @Override
215 |  |
                 public void pending(String step) {
216 | 0 |
                     if (pendingStepStrategy instanceof FailingUponPendingStep) {
217 | 0 |
                         failingStories.add(currentStory.get().getPath());
218 |  |
                     }
219 | 0 |
                     super.pending(step);
220 | 0 |
                 }
221 |  |
222 |  |
                 @Override
223 |  |
                 public void failed(String step, Throwable cause) {
224 | 8 |
                     failingStories.add(currentStory.get().getPath());
225 | 8 |
                     super.failed(step, cause);
226 | 8 |
                 }
227 |  |
228 |  |
                 @Override
229 |  |
                 public void afterStory(boolean givenStory) {
230 | 4 |
                     if (givenStory)
231 | 0 |
                         return;
232 | 4 |
                     times.put(currentStory.get().getPath(), System.currentTimeMillis() - currentStoryStart.get());
233 | 4 |
                     if (outputAfterEachStory) {
234 | 0 |
                         outputToFiles(storyReporterBuilder);
235 |  |
                     }
236 | 4 |
                     super.afterStory(givenStory);
237 | 4 |
                 }
238 |  |
239 |  |
                 @Override
240 |  |
                 public void beforeScenario(String title) {
241 | 8 |
                     currentScenarioTitle.set(title);
242 | 8 |
                     super.beforeScenario(title);
243 | 8 |
                 }
244 |  |
             };
245 |  |
         }
246 |  |
247 | 10 |
         private class XRefStepMonitor extends NullStepMonitor {
248 |  |
             @Override
249 |  |
             public void performing(String step, boolean dryRun) {
250 | 0 |
                 super.performing(step, dryRun);
251 | 0 |
                 stepsPerformed.add(currentStory.get().getPath());
252 | 0 |
             }
253 |  |
254 |  |
             public void stepMatchesPattern(String step, boolean matches, StepPattern pattern, Method method,
255 |  |
                     Object stepsInstance) {
256 | 4 |
                 Story story = currentStory.get();
257 | 4 |
                 if (story == null) {
258 | 0 |
                     throw new NullPointerException("story not setup for CrossReference");
259 |  |
                 }
260 |  |
261 | 4 |
                 if (matches) {
262 | 4 |
                     String key = pattern.type() + pattern.annotated();
263 | 4 |
                     StepMatch stepMatch = stepMatches.get(key);
264 | 4 |
                     if (stepMatch == null) {
265 | 4 |
                         stepMatch = new StepMatch(pattern.type(), pattern.annotated(), pattern.resolved());
266 | 4 |
                         stepMatches.put(key, stepMatch);
267 |  |
                     }
268 |  |
                     // find canonical ref for same stepMatch
269 | 4 |
                     stepMatch.usages.add(new StepUsage(story.getPath(), currentScenarioTitle.get(), step));
270 |  |
                 }
271 | 4 |
                 super.stepMatchesPattern(step, matches, pattern, method, stepsInstance);
272 | 4 |
             }
273 |  |
         }
274 |  |
275 | 8 |
         public static class XRefRoot {
276 | 5 |
             protected long whenMade = System.currentTimeMillis();
277 | 5 |
             protected String createdBy = createdBy();
278 | 5 |
             protected String metaFilter = "";
279 |  |
280 | 5 |
             private Set<String> meta = new HashSet<String>();
281 | 5 |
             private List<XRefStory> stories = new ArrayList<XRefStory>();
282 | 5 |
             private List<StepMatch> stepMatches = new ArrayList<StepMatch>();
283 |  |
284 |  |
             private transient boolean excludeStoriesWithNoExecutedScenarios;
285 |  |
286 | 5 |
             public XRefRoot() {
287 | 5 |
             }
288 |  |
289 |  |
             public void setExcludeStoriesWithNoExecutedScenarios(boolean exclude) {
290 | 5 |
                 this.excludeStoriesWithNoExecutedScenarios = exclude;
291 | 5 |
             }
292 |  |
293 |  |
             protected String createdBy() {
294 | 5 |
                 return "JBehave";
295 |  |
             }
296 |  |
297 |  |
             protected void processStories(List<StoryHolder> stories, Set<String> stepsPerformed, Map<String, Long> times,
298 |  |
                     StoryReporterBuilder builder, Set<String> failures) {
299 |  |
                 // Prevent Concurrent Modification Exception.
300 | 5 |
                 synchronized (stories) {
301 | 5 |
                     for (StoryHolder storyHolder : stories) {
302 | 4 |
                         Story story = storyHolder.story;
303 | 4 |
                         String path = story.getPath();
304 | 4 |
                         if (!path.equals("BeforeStories") && !path.equals("AfterStories")) {
305 | 4 |
                             if (someScenarios(story, stepsPerformed) || !excludeStoriesWithNoExecutedScenarios) {
306 | 4 |
                                 XRefStory xRefStory = createXRefStory(builder, story, !failures.contains(path), this);
307 | 4 |
                                 xRefStory.started = storyHolder.when;
308 | 4 |
                                 xRefStory.duration = getTime(times, story);
309 | 4 |
                                 this.stories.add(xRefStory);
310 |  |
                             }
311 |  |
                         }
312 |  |
313 | 4 |
                     }
314 | 5 |
                 }
315 | 5 |
             }
316 |  |
317 |  |
             protected Long getTime(Map<String, Long> times, Story story) {
318 | 4 |
                 Long time = times.get(story.getPath());
319 | 4 |
                 if (time == null) {
320 | 0 |
                     return 0L;
321 |  |
                 }
322 | 4 |
                 return time;
323 |  |
             }
324 |  |
325 |  |
             protected boolean someScenarios(Story story, Set<String> stepsPerformed) {
326 | 4 |
                 return stepsPerformed.contains(story.getPath());
327 |  |
             }
328 |  |
329 |  |
             /**
330 |  |
              * Ensure that XRefStory is instantiated completely, before secondary
331 |  |
              * methods are invoked (or overridden)
332 |  |
              */
333 |  |
             protected final XRefStory createXRefStory(StoryReporterBuilder storyReporterBuilder, Story story,
334 |  |
                     boolean passed, XRefRoot root) {
335 | 4 |
                 XRefStory xrefStory = createXRefStory(storyReporterBuilder, story, passed);
336 | 4 |
                 xrefStory.processMetaTags(root);
337 | 4 |
                 xrefStory.processScenarios();
338 | 4 |
                 return xrefStory;
339 |  |
             }
340 |  |
341 |  |
             /**
342 |  |
              * Override this is you want to add fields to the JSON. Specifically,
343 |  |
              * create a subclass of XRefStory to return.
344 |  |
              *
345 |  |
              * @param storyReporterBuilder the story reporter builder
346 |  |
              * @param story the story
347 |  |
              * @param passed the story passed (or failed)
348 |  |
              * @return An XRefStory
349 |  |
              */
350 |  |
             protected XRefStory createXRefStory(StoryReporterBuilder storyReporterBuilder, Story story, boolean passed) {
351 | 3 |
                 return new XRefStory(story, storyReporterBuilder, passed);
352 |  |
             }
353 |  |
354 |  |
             protected void addStepMatches(Map<String, StepMatch> stepMatchMap) {
355 | 5 |
                 for (String key : stepMatchMap.keySet()) {
356 | 4 |
                     StepMatch stepMatch = stepMatchMap.get(key);
357 | 4 |
                     stepMatches.add(stepMatch);
358 | 4 |
                 }
359 | 5 |
             }
360 |  |
         }
361 |  |
362 |  |
         @SuppressWarnings("unused")
363 |  |
         public static class XRefStory {
364 |  |
             private transient Story story; // don't turn into JSON.
365 |  |
             private String description;
366 | 4 |
             private String narrative = "";
367 |  |
             private String name;
368 |  |
             private String path;
369 |  |
             private String html;
370 | 4 |
             private String meta = "";
371 | 4 |
             private String scenarios = "";
372 |  |
             private boolean passed;
373 |  |
             public long started;
374 |  |
             public long duration;
375 |  |
376 | 4 |
             public XRefStory(Story story, StoryReporterBuilder storyReporterBuilder, boolean passed) {
377 | 4 |
                 this.story = story;
378 | 4 |
                 Narrative narrative = story.getNarrative();
379 | 4 |
                 if (!narrative.isEmpty()) {
380 | 4 |
                     this.narrative = "In order to " + narrative.inOrderTo() + "\n" + "As a " + narrative.asA() + "\n"
381 |  |
                             + "I want to " + narrative.iWantTo() + "\n";
382 |  |
                 }
383 | 4 |
                 this.description = story.getDescription().asString();
384 | 4 |
                 this.name = story.getName();
385 | 4 |
                 this.path = story.getPath();
386 | 4 |
                 this.passed = passed;
387 | 4 |
                 this.html = storyReporterBuilder.pathResolver().resolveName(
388 |  |
                         new StoryLocation(storyReporterBuilder.codeLocation(), story.getPath()), "html");
389 | 4 |
             }
390 |  |
391 |  |
             protected void processScenarios() {
392 | 4 |
                 for (Scenario scenario : story.getScenarios()) {
393 | 0 |
                     String body = "Scenario:" + scenario.getTitle() + "\n";
394 | 0 |
                     List<String> steps = scenario.getSteps();
395 | 0 |
                     for (String step : steps) {
396 | 0 |
                         body = body + step + "\n";
397 |  |
                     }
398 | 0 |
                     scenarios = scenarios + body + "\n\n";
399 | 0 |
                 }
400 | 4 |
             }
401 |  |
402 |  |
             protected void processMetaTags(XRefRoot root) {
403 | 4 |
                 Meta storyMeta = story.getMeta();
404 | 4 |
                 for (String next : storyMeta.getPropertyNames()) {
405 | 8 |
                     String property = next + "=" + storyMeta.getProperty(next);
406 | 8 |
                     addMetaProperty(property, root.meta);
407 | 8 |
                     String newMeta = appendMetaProperty(property, this.meta);
408 | 8 |
                     if (newMeta != null) {
409 | 7 |
                         this.meta = newMeta;
410 |  |
                     }
411 | 8 |
                 }
412 | 4 |
             }
413 |  |
414 |  |
             protected String appendMetaProperty(String property, String meta) {
415 | 7 |
                 return meta + property + "\n";
416 |  |
             }
417 |  |
418 |  |
             protected void addMetaProperty(String property, Set<String> meta) {
419 | 7 |
                 meta.add(property);
420 | 7 |
             }
421 |  |
         }
422 |  |
423 |  |
         @SuppressWarnings("unused")
424 |  |
         public static class StepUsage {
425 |  |
             private final String story;
426 |  |
             private final String scenario;
427 |  |
             private final String step;
428 |  |
429 | 4 |
             public StepUsage(String story, String scenario, String step) {
430 | 4 |
                 this.story = story;
431 | 4 |
                 this.scenario = scenario;
432 | 4 |
                 this.step = step;
433 | 4 |
             }
434 |  |
         }
435 |  |
436 | 4 |
         public static class StepMatch {
437 |  |
             private final StepType type; // key
438 |  |
             private final String annotatedPattern; // key
439 |  |
             // these not in hashcode or equals()
440 |  |
             @SuppressWarnings("unused")
441 |  |
             private final String resolvedPattern;
442 | 4 |
             private final Set<StepUsage> usages = new HashSet<StepUsage>();
443 |  |
444 | 4 |
             public StepMatch(StepType type, String annotatedPattern, String resolvedPattern) {
445 | 4 |
                 this.type = type;
446 | 4 |
                 this.annotatedPattern = annotatedPattern;
447 | 4 |
                 this.resolvedPattern = resolvedPattern;
448 | 4 |
             }
449 |  |
450 |  |
             @Override
451 |  |
             public boolean equals(Object o) {
452 | 0 |
                 if (this == o)
453 | 0 |
                     return true;
454 | 0 |
                 if (o == null || getClass() != o.getClass())
455 | 0 |
                     return false;
456 |  |
457 | 0 |
                 StepMatch stepMatch = (StepMatch) o;
458 |  |
459 | 0 |
                 if (annotatedPattern != null ? !annotatedPattern.equals(stepMatch.annotatedPattern)
460 |  |
                         : stepMatch.annotatedPattern != null)
461 | 0 |
                     return false;
462 | 0 |
                 if (type != stepMatch.type)
463 | 0 |
                     return false;
464 |  |
465 | 0 |
                 return true;
466 |  |
             }
467 |  |
468 |  |
             @Override
469 |  |
             public int hashCode() {
470 | 0 |
                 int result = type != null ? type.hashCode() : 0;
471 | 0 |
                 result = 31 * result + (annotatedPattern != null ? annotatedPattern.hashCode() : 0);
472 | 0 |
                 return result;
473 |  |
             }
474 |  |
         }
475 |  |
476 | 4 |
         private class StoryHolder {
477 |  |
             Story story;
478 |  |
             long when;
479 |  |
480 | 4 |
             private StoryHolder(Story story) {
481 | 4 |
                 this.story = story;
482 | 4 |
                 this.when = System.currentTimeMillis();
483 | 4 |
             }
484 |  |
485 |  |
         }
486 |  |
     }