]> source.dussan.org Git - sonarqube.git/blob
72675effcc2b1e49cae8aa78cc5de87aff2a3eaf
[sonarqube.git] /
1 /*
2  * SonarQube
3  * Copyright (C) 2009-2020 SonarSource SA
4  * mailto:info AT sonarsource DOT com
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 3 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software Foundation,
18  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19  */
20 package org.sonar.server.platform.db.migration.version.v86;
21
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.io.StringReader;
26 import java.io.StringWriter;
27 import java.io.Writer;
28 import java.sql.SQLException;
29 import java.util.AbstractMap;
30 import java.util.ArrayList;
31 import java.util.Collection;
32 import java.util.LinkedHashMap;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.Objects;
36 import java.util.Set;
37 import java.util.TreeSet;
38 import java.util.regex.Pattern;
39 import java.util.stream.Collectors;
40 import javax.annotation.CheckForNull;
41 import javax.annotation.Nullable;
42 import javax.xml.XMLConstants;
43 import javax.xml.parsers.ParserConfigurationException;
44 import javax.xml.parsers.SAXParser;
45 import javax.xml.parsers.SAXParserFactory;
46 import javax.xml.stream.XMLInputFactory;
47 import javax.xml.stream.XMLStreamException;
48 import javax.xml.transform.sax.SAXSource;
49 import javax.xml.validation.SchemaFactory;
50 import org.apache.commons.lang.StringUtils;
51 import org.codehaus.staxmate.SMInputFactory;
52 import org.codehaus.staxmate.in.SMHierarchicCursor;
53 import org.codehaus.staxmate.in.SMInputCursor;
54 import org.sonar.api.resources.Qualifiers;
55 import org.sonar.api.utils.System2;
56 import org.sonar.core.util.UuidFactory;
57 import org.sonar.db.Database;
58 import org.sonar.server.platform.db.migration.step.DataChange;
59 import org.sonar.server.platform.db.migration.step.Select;
60 import org.sonar.server.platform.db.migration.step.Upsert;
61 import org.xml.sax.InputSource;
62 import org.xml.sax.SAXException;
63 import org.xml.sax.SAXParseException;
64 import org.xml.sax.helpers.DefaultHandler;
65
66 import static java.lang.String.format;
67 import static java.nio.charset.StandardCharsets.UTF_8;
68 import static javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING;
69 import static org.apache.commons.io.IOUtils.toInputStream;
70 import static org.apache.commons.lang.StringEscapeUtils.escapeXml;
71 import static org.apache.commons.lang.StringUtils.trim;
72
73 public class MigrateApplicationDefinitionsFromXmlToDb extends DataChange {
74   static final int TEXT_VALUE_MAX_LENGTH = 4000;
75   private static final String SELECT_APPLICATION_UUID_BY_KEY = "select uuid from projects where kee = ? and qualifier = 'APP'";
76   private static final String SELECT_PROJECTS_BY_KEYS = "select kee,uuid from projects where kee in (%s) and qualifier = 'TRK'";
77   private static final String UPDATE_INTERNAL_PROP_TEXT_VALUE = "update internal_properties set text_value = ?, clob_value = NULL where kee = ?";
78   private static final String UPDATE_INTERNAL_PROP_CLOB_VALUE = "update internal_properties set clob_value = ?, text_value = NULL where kee = ?";
79   private static final String VIEWS_DEF_KEY = "views.def";
80
81   private final UuidFactory uuidFactory;
82   private final System2 system;
83
84   public MigrateApplicationDefinitionsFromXmlToDb(Database db, UuidFactory uuidFactory, System2 system) {
85     super(db);
86
87     this.uuidFactory = uuidFactory;
88     this.system = system;
89   }
90
91   @Override
92   protected void execute(Context context) throws SQLException {
93     String xml = getViewsDefinition(context);
94     // skip migration if `views.def` does not exist in the db
95     if (xml == null) {
96       return;
97     }
98
99     try {
100       Map<String, ViewXml.ViewDef> defs = ViewXml.parse(xml);
101       List<ViewXml.ViewDef> applications = defs.values()
102         .stream()
103         .filter(v -> Qualifiers.APP.equals(v.getQualifier()))
104         .collect(Collectors.toList());
105       for (ViewXml.ViewDef app : applications) {
106         insertApplication(context, app);
107       }
108       cleanUpViewsDefinitionsXml(context, defs.values());
109     } catch (Exception e) {
110       throw new IllegalStateException("Failed to migrate views definitions property.", e);
111     }
112
113   }
114
115   private void insertApplication(Context context, ViewXml.ViewDef app) throws SQLException {
116     long now = system.now();
117     String applicationUuid = context.prepareSelect(SELECT_APPLICATION_UUID_BY_KEY)
118       .setString(1, app.getKey())
119       .get(r -> r.getString(1));
120
121     // ignore if application only exists in xml and not in the db. It will be removed from the xml at later stage of the migration.
122     if (applicationUuid == null) {
123       return;
124     }
125
126     String queryParam = app.getProjects().stream().map(uuid -> "'" + uuid + "'").collect(Collectors.joining(","));
127     Map<String, String> projectUuidsByKeys = context.prepareSelect(format(SELECT_PROJECTS_BY_KEYS, queryParam))
128       .list(r -> new AbstractMap.SimpleEntry<>(r.getString(1), r.getString(2)))
129       .stream()
130       .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
131
132     insertApplicationProjects(context, app, applicationUuid, projectUuidsByKeys, now);
133     if (!app.getApplicationBranches().isEmpty()) {
134       insertApplicationBranchesProjects(context, app, applicationUuid, projectUuidsByKeys, now);
135     }
136   }
137
138   private void insertApplicationProjects(Context context, ViewXml.ViewDef app, String applicationUuid,
139     Map<String, String> projectUuidsByKeys, long createdTime) throws SQLException {
140     Upsert insertApplicationProjectsQuery = context.prepareUpsert("insert into " +
141       "app_projects(uuid, application_uuid, project_uuid, created_at) " +
142       "values (?, ?, ?, ?)");
143     for (String projectKey : app.getProjects()) {
144       String applicationProjectUuid = uuidFactory.create();
145       String projectUuid = projectUuidsByKeys.get(projectKey);
146
147       // ignore project if it does not exist anymore
148       if (projectUuid == null) {
149         continue;
150       }
151
152       insertApplicationProjectsQuery
153         .setString(1, applicationProjectUuid)
154         .setString(2, applicationUuid)
155         .setString(3, projectUuid)
156         .setLong(4, createdTime)
157         .addBatch();
158     }
159
160     insertApplicationProjectsQuery.execute().commit();
161   }
162
163   private void insertApplicationBranchesProjects(Context context, ViewXml.ViewDef app, String applicationUuid,
164     Map<String, String> projectUuidsByKeys, long createdTime) throws SQLException {
165     Map<String, String> appBranchUuidByKey = context.prepareSelect("select kee,uuid from project_branches where project_uuid = ?")
166       .setString(1, applicationUuid)
167       .list(r -> new AbstractMap.SimpleEntry<>(r.getString(1), r.getString(2)))
168       .stream()
169       .collect(Collectors.toMap(AbstractMap.SimpleEntry::getKey, AbstractMap.SimpleEntry::getValue));
170
171     Upsert insertApplicationsBranchProjectsQuery = context.prepareUpsert("insert into " +
172       "app_branch_project_branch(uuid, application_uuid, application_branch_uuid, project_uuid, project_branch_uuid, created_at) " +
173       "values (?, ?, ?, ?, ?, ?)");
174     boolean insert = false;
175     for (ViewXml.ApplicationBranchDef branch : app.getApplicationBranches()) {
176       String applicationBranchUuid = appBranchUuidByKey.get(branch.getKey());
177       // ignore application branch if it does not exist in the DB anymore
178       if (applicationBranchUuid == null) {
179         continue;
180       }
181
182       if (insertApplicationBranchProjects(context, branch, applicationUuid, applicationBranchUuid, projectUuidsByKeys, createdTime,
183         insertApplicationsBranchProjectsQuery)) {
184         insert = true;
185       }
186     }
187
188     if (insert) {
189       insertApplicationsBranchProjectsQuery.execute().commit();
190     }
191   }
192
193   private boolean insertApplicationBranchProjects(Context context, ViewXml.ApplicationBranchDef branch, String applicationUuid,
194     String applicationBranchUuid, Map<String, String> projectUuidsByKeys, long createdTime,
195     Upsert insertApplicationsBranchProjectsQuery) throws SQLException {
196
197     boolean insert = false;
198     for (ViewXml.ApplicationProjectDef appProjDef : branch.getProjects()) {
199       String projectUuid = projectUuidsByKeys.get(appProjDef.getKey());
200
201       // ignore projects that do not exist in the DB anymore
202       if (projectUuid != null) {
203         String projectBranchUuid = context.prepareSelect("select uuid from project_branches where project_uuid = ? and kee = ?")
204           .setString(1, projectUuid)
205           .setString(2, appProjDef.getBranch())
206           .get(r -> r.getString(1));
207
208         // ignore project branches that do not exist in the DB anymore
209         if (projectBranchUuid != null) {
210           String applicationBranchProjectUuid = uuidFactory.create();
211           insertApplicationsBranchProjectsQuery
212             .setString(1, applicationBranchProjectUuid)
213             .setString(2, applicationUuid)
214             .setString(3, applicationBranchUuid)
215             .setString(4, projectUuid)
216             .setString(5, projectBranchUuid)
217             .setLong(6, createdTime)
218             .addBatch();
219           insert = true;
220         }
221       }
222     }
223
224     return insert;
225   }
226
227   @CheckForNull
228   private static String getViewsDefinition(DataChange.Context context) throws SQLException {
229     Select select = context.prepareSelect("select text_value,clob_value from internal_properties where kee=?");
230     select.setString(1, VIEWS_DEF_KEY);
231     return select.get(row -> {
232       String v = row.getString(1);
233       if (v != null) {
234         return v;
235       } else {
236         return row.getString(2);
237       }
238     });
239   }
240
241   private static void cleanUpViewsDefinitionsXml(Context context, Collection<ViewXml.ViewDef> definitions) throws SQLException, IOException {
242     definitions = definitions.stream()
243       .filter(d -> !"APP".equals(d.getQualifier()))
244       .collect(Collectors.toList());
245
246     StringWriter output = new StringWriter();
247     new ViewXml.ViewDefinitionsSerializer().write(definitions, output);
248     String value = output.toString();
249     String statement = UPDATE_INTERNAL_PROP_TEXT_VALUE;
250     if (mustBeStoredInClob(value)) {
251       statement = UPDATE_INTERNAL_PROP_CLOB_VALUE;
252     }
253
254     context.prepareUpsert(statement)
255       .setString(1, output.toString())
256       .setString(2, VIEWS_DEF_KEY)
257       .execute()
258       .commit();
259   }
260
261   private static boolean mustBeStoredInClob(String value) {
262     return value.length() > TEXT_VALUE_MAX_LENGTH;
263   }
264
265   private static class ViewXml {
266     static final String SCHEMA_VIEWS = "/static/views.xsd";
267     static final String VIEWS_HEADER_BARE = "<views>";
268     static final Pattern VIEWS_HEADER_BARE_PATTERN = Pattern.compile(VIEWS_HEADER_BARE);
269     static final String VIEWS_HEADER_FQ = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
270       + "<views xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns=\"http://sonarsource.com/schema/views\">";
271
272     private ViewXml() {
273       // nothing to do here
274     }
275
276     private static Map<String, ViewDef> parse(String xml) throws ParserConfigurationException, SAXException, IOException, XMLStreamException {
277       if (StringUtils.isEmpty(xml)) {
278         return new LinkedHashMap<>(0);
279       }
280
281       List<ViewDef> views;
282       validate(xml);
283       SMInputFactory inputFactory = initStax();
284       SMHierarchicCursor rootC = inputFactory.rootElementCursor(new StringReader(xml));
285       rootC.advance(); // <views>
286       SMInputCursor cursor = rootC.childElementCursor();
287       views = parseViewDefinitions(cursor);
288
289       Map<String, ViewDef> result = new LinkedHashMap<>(views.size());
290       for (ViewDef def : views) {
291         result.put(def.getKey(), def);
292       }
293
294       return result;
295     }
296
297     private static void validate(String xml) throws IOException, SAXException, ParserConfigurationException {
298       // Replace bare, namespace unaware header with fully qualified header (with schema declaration)
299       String fullyQualifiedXml = VIEWS_HEADER_BARE_PATTERN.matcher(xml).replaceFirst(VIEWS_HEADER_FQ);
300       try (InputStream xsd = MigrateApplicationDefinitionsFromXmlToDb.class.getResourceAsStream(SCHEMA_VIEWS)) {
301         InputSource viewsDefinition = new InputSource(new InputStreamReader(toInputStream(fullyQualifiedXml, UTF_8), UTF_8));
302
303         SchemaFactory saxSchemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
304         saxSchemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
305         saxSchemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
306
307         SAXParserFactory parserFactory = SAXParserFactory.newInstance();
308         parserFactory.setFeature(FEATURE_SECURE_PROCESSING, true);
309         parserFactory.setNamespaceAware(true);
310         parserFactory.setSchema(saxSchemaFactory.newSchema(new SAXSource(new InputSource(xsd))));
311
312         SAXParser saxParser = parserFactory.newSAXParser();
313         saxParser.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, "");
314         saxParser.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
315         saxParser.parse(viewsDefinition, new ViewsValidator());
316       }
317     }
318
319     private static List<ViewDef> parseViewDefinitions(SMInputCursor viewsCursor) throws XMLStreamException {
320       List<ViewDef> views = new ArrayList<>();
321       while (viewsCursor.getNext() != null) {
322         ViewDef viewDef = new ViewDef();
323         viewDef.setKey(viewsCursor.getAttrValue("key"));
324         viewDef.setDef(Boolean.parseBoolean(viewsCursor.getAttrValue("def")));
325         viewDef.setParent(viewsCursor.getAttrValue("parent"));
326         viewDef.setRoot(viewsCursor.getAttrValue("root"));
327         SMInputCursor viewCursor = viewsCursor.childElementCursor();
328         while (viewCursor.getNext() != null) {
329           String nodeName = viewCursor.getLocalName();
330           parseChildElement(viewDef, viewCursor, nodeName);
331         }
332         views.add(viewDef);
333       }
334       return views;
335     }
336
337     private static void parseChildElement(ViewDef viewDef, SMInputCursor viewCursor, String nodeName) throws XMLStreamException {
338       if (StringUtils.equals(nodeName, "name")) {
339         viewDef.setName(trim(viewCursor.collectDescendantText()));
340       } else if (StringUtils.equals(nodeName, "desc")) {
341         viewDef.setDesc(trim(viewCursor.collectDescendantText()));
342       } else if (StringUtils.equals(nodeName, "regexp")) {
343         viewDef.setRegexp(trim(viewCursor.collectDescendantText()));
344       } else if (StringUtils.equals(nodeName, "language")) {
345         viewDef.setLanguage(trim(viewCursor.collectDescendantText()));
346       } else if (StringUtils.equals(nodeName, "tag_key")) {
347         viewDef.setTagKey(trim(viewCursor.collectDescendantText()));
348       } else if (StringUtils.equals(nodeName, "tag_value")) {
349         viewDef.setTagValue(trim(viewCursor.collectDescendantText()));
350       } else if (StringUtils.equals(nodeName, "p")) {
351         viewDef.addProject(trim(viewCursor.collectDescendantText()));
352       } else if (StringUtils.equals(nodeName, "vw-ref")) {
353         viewDef.addReference(trim(viewCursor.collectDescendantText()));
354       } else if (StringUtils.equals(nodeName, "qualifier")) {
355         viewDef.setQualifier(trim(viewCursor.collectDescendantText()));
356       } else if (StringUtils.equals(nodeName, "branch")) {
357         parseBranch(viewDef, viewCursor);
358       } else if (StringUtils.equals(nodeName, "tagsAssociation")) {
359         parseTagsAssociation(viewDef, viewCursor);
360       }
361     }
362
363     private static void parseBranch(ViewDef def, SMInputCursor viewCursor) throws XMLStreamException {
364       List<ApplicationProjectDef> projects = new ArrayList<>();
365       String key = viewCursor.getAttrValue("key");
366       SMInputCursor projectCursor = viewCursor.childElementCursor();
367       while (projectCursor.getNext() != null) {
368         if (Objects.equals(projectCursor.getLocalName(), "p")) {
369           String branch = projectCursor.getAttrValue("branch");
370           String projectKey = trim(projectCursor.collectDescendantText());
371           projects.add(new ApplicationProjectDef().setKey(projectKey).setBranch(branch));
372         }
373       }
374       def.getApplicationBranches().add(new ApplicationBranchDef()
375         .setKey(key)
376         .setProjects(projects));
377     }
378
379     private static void parseTagsAssociation(ViewDef def, SMInputCursor viewCursor) throws XMLStreamException {
380       SMInputCursor projectCursor = viewCursor.childElementCursor();
381       while (projectCursor.getNext() != null) {
382         def.addTagAssociation(trim(projectCursor.collectDescendantText()));
383       }
384     }
385
386     private static SMInputFactory initStax() {
387       XMLInputFactory xmlFactory = XMLInputFactory.newInstance();
388       xmlFactory.setProperty(XMLInputFactory.IS_COALESCING, Boolean.TRUE);
389       xmlFactory.setProperty(XMLInputFactory.IS_NAMESPACE_AWARE, Boolean.FALSE);
390       // just so it won't try to load DTD in if there's DOCTYPE
391       xmlFactory.setProperty(XMLInputFactory.SUPPORT_DTD, Boolean.FALSE);
392       xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE);
393       return new SMInputFactory(xmlFactory);
394     }
395
396     private static class ViewDef {
397       String key = null;
398
399       String parent = null;
400
401       String root = null;
402
403       boolean def = false;
404
405       List<String> p = new ArrayList<>();
406
407       List<String> vwRef = new ArrayList<>();
408
409       String name = null;
410
411       String desc = null;
412
413       String regexp = null;
414
415       String language = null;
416
417       String tagKey = null;
418
419       String tagValue = null;
420
421       String qualifier = null;
422
423       List<ApplicationBranchDef> applicationBranches = new ArrayList<>();
424
425       Set<String> tagsAssociation = new TreeSet<>();
426
427       public String getKey() {
428         return key;
429       }
430
431       public String getParent() {
432         return parent;
433       }
434
435       @CheckForNull
436       public String getRoot() {
437         return root;
438       }
439
440       public boolean isDef() {
441         return def;
442       }
443
444       public List<String> getProjects() {
445         return p;
446       }
447
448       public List<String> getReferences() {
449         return vwRef;
450       }
451
452       public String getName() {
453         return name;
454       }
455
456       public String getDesc() {
457         return desc;
458       }
459
460       @CheckForNull
461       public String getRegexp() {
462         return regexp;
463       }
464
465       @CheckForNull
466       public String getLanguage() {
467         return language;
468       }
469
470       @CheckForNull
471       public String getTagKey() {
472         return tagKey;
473       }
474
475       @CheckForNull
476       public String getTagValue() {
477         return tagValue;
478       }
479
480       @CheckForNull
481       public String getQualifier() {
482         return qualifier;
483       }
484
485       public List<ApplicationBranchDef> getApplicationBranches() {
486         return applicationBranches;
487       }
488
489       public Set<String> getTagsAssociation() {
490         return tagsAssociation;
491       }
492
493       public ViewDef setKey(String key) {
494         this.key = key;
495         return this;
496       }
497
498       public ViewDef setParent(String parent) {
499         this.parent = parent;
500         return this;
501       }
502
503       public ViewDef setRoot(@Nullable String root) {
504         this.root = root;
505         return this;
506       }
507
508       public ViewDef setDef(boolean def) {
509         this.def = def;
510         return this;
511       }
512
513       public ViewDef setProjects(List<String> projects) {
514         this.p = projects;
515         return this;
516       }
517
518       public ViewDef addProject(String project) {
519         this.p.add(project);
520         return this;
521       }
522
523       public ViewDef removeProject(String project) {
524         this.p.remove(project);
525         return this;
526       }
527
528       public ViewDef setName(String name) {
529         this.name = name;
530         return this;
531       }
532
533       public ViewDef setDesc(@Nullable String desc) {
534         this.desc = desc;
535         return this;
536       }
537
538       public ViewDef setRegexp(@Nullable String regexp) {
539         this.regexp = regexp;
540         return this;
541       }
542
543       public ViewDef setLanguage(@Nullable String language) {
544         this.language = language;
545         return this;
546       }
547
548       public ViewDef setTagKey(@Nullable String tagKey) {
549         this.tagKey = tagKey;
550         return this;
551       }
552
553       public ViewDef setTagValue(@Nullable String tagValue) {
554         this.tagValue = tagValue;
555         return this;
556       }
557
558       public ViewDef addReference(String reference) {
559         this.vwRef.add(reference);
560         return this;
561       }
562
563       public ViewDef removeReference(String reference) {
564         this.vwRef.remove(reference);
565         return this;
566       }
567
568       public ViewDef setReferences(List<String> vwRef) {
569         this.vwRef = vwRef;
570         return this;
571       }
572
573       public ViewDef setQualifier(@Nullable String qualifier) {
574         this.qualifier = qualifier;
575         return this;
576       }
577
578       public ViewDef setApplicationBranches(List<ApplicationBranchDef> branches) {
579         this.applicationBranches = branches;
580         return this;
581       }
582
583       public ViewDef addTagAssociation(String tag) {
584         this.tagsAssociation.add(tag);
585         return this;
586       }
587
588       public ViewDef setTagsAssociation(Set<String> tagsAssociation) {
589         this.tagsAssociation = tagsAssociation;
590         return this;
591       }
592     }
593
594     private static class ApplicationProjectDef {
595       private String key = null;
596       private String branch = null;
597
598       public String getKey() {
599         return key;
600       }
601
602       public ApplicationProjectDef setKey(String key) {
603         this.key = key;
604         return this;
605       }
606
607       @CheckForNull
608       public String getBranch() {
609         return branch;
610       }
611
612       public ApplicationProjectDef setBranch(@Nullable String branch) {
613         this.branch = branch;
614         return this;
615       }
616     }
617
618     private static class ApplicationBranchDef {
619
620       private String key = null;
621       private List<ApplicationProjectDef> p = new ArrayList<>();
622
623       public String getKey() {
624         return key;
625       }
626
627       public ApplicationBranchDef setKey(String key) {
628         this.key = key;
629         return this;
630       }
631
632       public List<ApplicationProjectDef> getProjects() {
633         return p;
634       }
635
636       public ApplicationBranchDef setProjects(List<ApplicationProjectDef> p) {
637         this.p = p;
638         return this;
639       }
640     }
641
642     private static final class ViewsValidator extends DefaultHandler {
643       @Override
644       public void error(SAXParseException exception) throws SAXException {
645         throw exception;
646       }
647
648       @Override
649       public void warning(SAXParseException exception) throws SAXException {
650         throw exception;
651       }
652
653       @Override
654       public void fatalError(SAXParseException exception) throws SAXException {
655         throw exception;
656       }
657     }
658
659     static class ViewDefinitionsSerializer {
660
661       public void write(Collection<ViewDef> definitions, Writer writer) throws IOException {
662         writer.append(VIEWS_HEADER_BARE);
663
664         for (ViewDef def : definitions) {
665           writer.append("<vw");
666           writer.append(" key=\"").append(escapeXml(def.getKey())).append("\"");
667           writer.append(" def=\"").append(Boolean.toString(def.isDef())).append("\"");
668           String parent = def.getParent();
669           if (parent != null) {
670             writer.append(" root=\"").append(escapeXml(def.getRoot())).append("\"");
671             writer.append(" parent=\"").append(escapeXml(parent)).append("\"");
672           }
673           writer.append(">");
674
675           writer.append("<name><![CDATA[").append(def.getName()).append("]]></name>");
676           writeOptionalElements(writer, def);
677
678           for (String project : def.getProjects()) {
679             writer.append("<p>").append(project).append("</p>");
680           }
681           for (String ref : def.getReferences()) {
682             writer.append("<vw-ref><![CDATA[").append(ref).append("]]></vw-ref>");
683           }
684           writeTagsAssociation(writer, def);
685           writer.append("</vw>");
686         }
687
688         writer.append("</views>");
689       }
690
691       private static void writeOptionalElements(Writer writer, ViewDef def) throws IOException {
692         String description = def.getDesc();
693         if (description != null) {
694           writer.append("<desc><![CDATA[").append(description).append("]]></desc>");
695         }
696         String regexp = def.getRegexp();
697         if (regexp != null) {
698           writer.append("<regexp><![CDATA[").append(regexp).append("]]></regexp>");
699         }
700         String language = def.getLanguage();
701         if (language != null) {
702           writer.append("<language><![CDATA[").append(language).append("]]></language>");
703         }
704         String customMeasureKey = def.getTagKey();
705         if (customMeasureKey != null) {
706           writer.append("<tag_key><![CDATA[").append(customMeasureKey).append("]]></tag_key>");
707         }
708         String customMeasureValue = def.getTagValue();
709         if (customMeasureValue != null) {
710           writer.append("<tag_value><![CDATA[").append(customMeasureValue).append("]]></tag_value>");
711         }
712         String qualifier = def.getQualifier();
713         if (qualifier != null) {
714           writer.append("<qualifier><![CDATA[").append(qualifier).append("]]></qualifier>");
715         }
716       }
717
718       private static void writeTagsAssociation(Writer writer, ViewDef definition) throws IOException {
719         Set<String> tagsAssociation = definition.getTagsAssociation();
720         if (tagsAssociation.isEmpty()) {
721           return;
722         }
723         writer.append("<tagsAssociation>");
724         for (String tag : tagsAssociation) {
725           writer.append("<tag>").append(tag).append("</tag>");
726         }
727         writer.append("</tagsAssociation>");
728       }
729
730     }
731   }
732 }