org.redmine.ta.internal.RedmineXMLParser Maven / Gradle / Ivy
Show all versions of redmine-java-api Show documentation
/*
Copyright 2010-2011 Alexey Skorokhodov.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.redmine.ta.internal;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.exolab.castor.mapping.Mapping;
import org.exolab.castor.mapping.MappingException;
import org.exolab.castor.xml.Unmarshaller;
import org.redmine.ta.beans.*;
import org.xml.sax.InputSource;
public class RedmineXMLParser {
private static final int UNKNOWN = -1;
private static final String MAPPING_PROJECTS_LIST = "/mapping_projects_list.xml";
private static final String MAPPING_ISSUES = "/mapping_issues_list.xml";
private static final String MAPPING_USERS = "/mapping_users.xml";
private static final String MAPPING_STATUSES = "/mapping_statuses_list.xml";
private static final String MAPPING_VERSIONS = "/mapping_versions_list.xml";
private static final String MAPPING_CATEGORIES = "/mapping_categories_list.xml";
private static final String MAPPING_TRACKERS = "/mapping_trackers_list.xml";
private static final String MAPPING_ATTACHMENTS = "/mapping_attachments_list.xml";
private static final String MAPPING_NEWS = "/mapping_news_list.xml";
// TODO optimize : pre-load xml
@SuppressWarnings("rawtypes")
private static final Map fromRedmineMap = new HashMap() {
private static final long serialVersionUID = 1L;
{
put(User.class, MAPPING_USERS);
put(Issue.class, MAPPING_ISSUES);
put(Project.class, MAPPING_PROJECTS_LIST);
put(TimeEntry.class, "/mapping_time_entries.xml");
put(SavedQuery.class, "/mapping_queries.xml");
put(IssueRelation.class, "/mapping_relations.xml");
put(IssueStatus.class, MAPPING_STATUSES);
put(Version.class, MAPPING_VERSIONS);
put(IssueCategory.class, MAPPING_CATEGORIES);
put(Tracker.class,MAPPING_TRACKERS);
put(Attachment.class,MAPPING_ATTACHMENTS);
put(News.class,MAPPING_NEWS);
}
};
public static Project parseProjectFromXML(String xml)
throws RuntimeException {
return parseObjectFromXML(Project.class, xml);
}
// see bug https://www.hostedredmine.com/issues/8240
@SuppressWarnings("rawtypes")
private static void removeBadTags(Class redmineClass, StringBuilder xml) {
if (redmineClass.equals(Issue.class)) {
replaceAll(xml, " ", "");
replaceAll(xml, " ", "");
}
}
private static void replaceAll(StringBuilder builder, String from, String to) {
int index = builder.indexOf(from);
while (index != -1) {
builder.replace(index, index + from.length(), to);
index += to.length(); // Move to the end of the replacement
index = builder.indexOf(from, index);
}
}
/**
* XML contains this line near the top:
*
* <?xml version="1.0" encoding="UTF-8"?><issues type="array" limit="25" total_count="103" offset="0">
* <?xml version="1.0" encoding="UTF-8"?><projects type="array" total_count="84" limit="25" offset="0">
*
* need to parse "total_count" value
*
* @return -1 (UNKNOWN) if can't parse - which means that the string is
* invalid / generated by an old Redmine version
*/
public static int parseObjectsTotalCount(String objectsXML) {
String reg = "<\\?xml version=\"1.0\" encoding=\"UTF-8\"\\?><.+ .*total_count=\"";
int maxCharsToCheck = Math.min(200, objectsXML.length());
String first200Chars = objectsXML.substring(0, maxCharsToCheck);
// String reg = "<\\?xml version=\"1.0\" encoding=\"UTF-8\"\\?><.+ type=\"array\".*total_count=\"";
Pattern pattern = Pattern.compile(reg);
Matcher matcher = pattern.matcher(first200Chars);
int result = UNKNOWN;
if (matcher.find()) {
int indexBeginNumber = matcher.end();
String tmp1 = first200Chars.substring(indexBeginNumber);
int end = tmp1.indexOf('"');
String numStr = tmp1.substring(0, end);
result = Integer.parseInt(numStr);
}
return result;
}
public static List parseProjectsFromXML(String xml) {
return parseObjectsFromXML(Project.class, xml);
}
private static Unmarshaller getUnmarshaller(String configFile,
Class> classToUse) {
// String configFile = configFilesMap.get(classToUse);
InputSource inputSource = new InputSource(
RedmineXMLParser.class.getResourceAsStream(configFile));
ClassLoader cl = RedmineXMLParser.class.getClassLoader();
// Note: Castor XML is packed in a separate OSGI bundle, so
// must set the classloader so that Castor will see our classes
Mapping mapping = new Mapping(cl);
mapping.loadMapping(inputSource);
Unmarshaller unmarshaller;
try {
unmarshaller = new Unmarshaller(mapping);
} catch (MappingException e) {
throw new RuntimeException(e);
}
unmarshaller.setClass(classToUse);
unmarshaller.setWhitespacePreserve(true);
return unmarshaller;
}
/**
* @throws RuntimeException if the text does not start with a valid XML tag.
*/
static void verifyStartsAsXML(String text) {
String XML_START_PATTERN = " List parseObjectsFromXML(Class classs, String body) {
verifyStartsAsXML(body);
StringBuilder builder = new StringBuilder(body);
removeBadTags(classs, builder);
String configFile = fromRedmineMap.get(classs);
Unmarshaller unmarshaller = getUnmarshaller(configFile, ArrayList.class);
List list = null;
StringReader reader = null;
try {
reader = new StringReader(builder.toString());
list = (ArrayList) unmarshaller.unmarshal(reader);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) {
reader.close();
}
}
return list;
}
public static T parseObjectFromXML(Class classs, String xml) {
verifyStartsAsXML(xml);
StringBuilder builder = new StringBuilder(xml);
removeBadTags(classs, builder);
String configFile = fromRedmineMap.get(classs);
Unmarshaller unmarshaller = getUnmarshaller(configFile, classs);
T obj = null;
StringReader reader = null;
try {
reader = new StringReader(builder.toString());
obj = classs.cast(unmarshaller.unmarshal(reader));
} catch (Exception e) {
e.printStackTrace();
} finally {
if (reader != null) {
reader.close();
}
}
return obj;
}
public static List parseUsersFromXML(String body) {
return parseObjectsFromXML(User.class, body);
}
public static User parseUserFromXML(String body) {
return parseObjectFromXML(User.class, body);
}
/**
* @param responseBody sample parameter:
*
* <?xml version="1.0" encoding="UTF-8"?>
* <errors>
* <error>Name can't be blank</error>
* <error>Identifier has already been taken</error>
* </errors>
*
*/
public static List parseErrors(String responseBody) {
List errors = new ArrayList();
/* I don't want to use Castor XML here with all these "include mapping" for errors file
* and making sure the mapping files are accessible in a plugin/jar/classpath and so on */
String lines[] = responseBody.split("\\r?\\n");
// skip first two lines: xml declaration and tag
int lineToStartWith = 2;
// skip last line with tag
int lastLine = lines.length - 1;
String openTag = "";
String closeTag = " ";
for (int i = lineToStartWith; i < lastLine; i++) {
int begin = lines[i].indexOf(openTag) + openTag.length();
int end = lines[i].indexOf(closeTag);
errors.add(lines[i].substring(begin, end));
}
return errors;
}
public static List parseTimeEntries(String xml) {
return parseObjectsFromXML(TimeEntry.class, xml);
}
public static IssueRelation parseRelationFromXML(String body) {
return parseObjectFromXML(IssueRelation.class, body);
}
public static List parseVersionsFromXML(String body) {
return parseObjectsFromXML(Version.class, body);
}
public static Version parseVersionFromXML(String body) {
return parseObjectFromXML(Version.class, body);
}
public static List parseIssueCategoriesFromXML(String body) {
return parseObjectsFromXML(IssueCategory.class, body);
}
}