Revize 38cde13c
Přidáno uživatelem stepanekp před asi 3 roky(ů)
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/controller/AppController.java | ||
---|---|---|
118 | 118 |
@PostMapping("/analyze") |
119 | 119 |
public String analyze(Model model, |
120 | 120 |
@RequestParam(value = "selectedProjects", required = false) String[] selectedProjects, |
121 |
@RequestParam(value = "selectedAntiPatterns", required = false) String[] selectedAntiPatterns |
|
121 |
@RequestParam(value = "selectedAntiPatterns", required = false) String[] selectedAntiPatterns, |
|
122 |
HttpSession session |
|
122 | 123 |
) { |
123 | 124 |
|
124 | 125 |
if (selectedProjects == null) { |
... | ... | |
135 | 136 |
return "index"; |
136 | 137 |
} |
137 | 138 |
|
138 |
List<QueryResult> results = antiPatternManager.analyze(selectedProjects, selectedAntiPatterns); |
|
139 |
String currentConfigurationName = configurationGetFromSession(session); |
|
140 |
Map<String, Map<String, String>> currentConfiguration = configurationService.getConfigurationByName(currentConfigurationName); |
|
141 |
|
|
142 |
List<QueryResult> results = antiPatternManager.analyze(selectedProjects, selectedAntiPatterns, currentConfiguration); |
|
139 | 143 |
antiPatternService.saveAnalyzedProjects(selectedProjects, selectedAntiPatterns); |
140 | 144 |
antiPatternService.saveResults(results); |
141 | 145 |
antiPatternService.setConfigurationChanged(false); |
... | ... | |
186 | 190 |
public String resultRecalculate(Model model) { |
187 | 191 |
|
188 | 192 |
List<QueryResult> results = antiPatternManager.analyze(antiPatternService.getAnalyzedProjects(), |
189 |
antiPatternService.getAnalyzedAntiPatterns()); |
|
193 |
antiPatternService.getAnalyzedAntiPatterns(), null);
|
|
190 | 194 |
|
191 | 195 |
antiPatternService.saveResults(results); |
192 | 196 |
antiPatternService.setConfigurationChanged(false); |
... | ... | |
426 | 430 |
if(session.getAttribute("configuration") != null) |
427 | 431 |
return session.getAttribute("configuration").toString(); // return configuration stored in session |
428 | 432 |
else{ |
429 |
List<String> configurationList = configurationService.getAllConfigurationNames();
|
|
433 |
List<String> configurationList = configurationService.getDefaultConfigurationNames();
|
|
430 | 434 |
if(configurationList.size() == 0) |
431 | 435 |
return null; |
432 | 436 |
|
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/AntiPatternManager.java | ||
---|---|---|
4 | 4 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.QueryResult; |
5 | 5 |
|
6 | 6 |
import java.util.List; |
7 |
import java.util.Map; |
|
7 | 8 |
|
8 | 9 |
public interface AntiPatternManager { |
9 | 10 |
|
10 |
List<QueryResult> analyze(String[] selectedProjects, String[] selectedAntiPatterns); |
|
11 |
List<QueryResult> analyze(String[] selectedProjects, String[] selectedAntiPatterns, Map<String, Map<String, String>> configuration);
|
|
11 | 12 |
} |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/AntiPatternManagerImpl.java | ||
---|---|---|
13 | 13 |
|
14 | 14 |
import java.util.ArrayList; |
15 | 15 |
import java.util.List; |
16 |
import java.util.Map; |
|
16 | 17 |
|
17 | 18 |
@Service |
18 | 19 |
public class AntiPatternManagerImpl implements AntiPatternManager { |
... | ... | |
24 | 25 |
private AntiPatternService antiPatternService; |
25 | 26 |
|
26 | 27 |
@Override |
27 |
public List<QueryResult> analyze(String[] selectedProjects, String[] selectedAntiPatterns) { |
|
28 |
public List<QueryResult> analyze(String[] selectedProjects, String[] selectedAntiPatterns, Map<String, Map<String, String>> configuration) {
|
|
28 | 29 |
|
29 | 30 |
return this.analyze(projectService.getAllProjectsForGivenIds(Utils.arrayOfStringsToArrayOfLongs(selectedProjects)), |
30 |
antiPatternService.getAllAntiPatternsForGivenIds(Utils.arrayOfStringsToArrayOfLongs(selectedAntiPatterns))); |
|
31 |
antiPatternService.getAllAntiPatternsForGivenIds(Utils.arrayOfStringsToArrayOfLongs(selectedAntiPatterns)), configuration);
|
|
31 | 32 |
} |
32 | 33 |
|
33 | 34 |
/** |
... | ... | |
36 | 37 |
* @param antiPatternDetectors Ap detectoros |
37 | 38 |
* @return List of results |
38 | 39 |
*/ |
39 |
private List<QueryResult> analyze(List<Project> projects, List<AntiPatternDetector> antiPatternDetectors) { |
|
40 |
private List<QueryResult> analyze(List<Project> projects, List<AntiPatternDetector> antiPatternDetectors, Map<String, Map<String, String>> configuration) {
|
|
40 | 41 |
DatabaseConnection databaseConnection = new DatabaseConnection(); |
41 | 42 |
|
42 | 43 |
List<QueryResult> queryResults = new ArrayList<>(); |
... | ... | |
46 | 47 |
QueryResult queryResult = new QueryResult(); |
47 | 48 |
queryResult.setProject(project); |
48 | 49 |
List<QueryResultItem> queryResultItems = new ArrayList<>(); |
50 |
|
|
49 | 51 |
for (AntiPatternDetector antiPattern : antiPatternDetectors) { |
50 |
queryResultItems.add(antiPattern.analyze(project, databaseConnection)); |
|
52 |
String antiPatternName = antiPattern.getAntiPatternModel().getName(); |
|
53 |
|
|
54 |
Map<String, String> thresholds = null; |
|
55 |
|
|
56 |
if(configuration != null) |
|
57 |
thresholds = configuration.get(antiPatternName); |
|
58 |
|
|
59 |
queryResultItems.add(antiPattern.analyze(project, databaseConnection, thresholds)); |
|
51 | 60 |
} |
52 | 61 |
queryResult.setQueryResultItems(queryResultItems); |
53 | 62 |
queryResults.add(queryResult); |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/detectors/AntiPatternDetector.java | ||
---|---|---|
4 | 4 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.AntiPattern; |
5 | 5 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.Project; |
6 | 6 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.QueryResultItem; |
7 |
import cz.zcu.fav.kiv.antipatterndetectionapp.utils.Utils; |
|
8 | 7 |
|
9 | 8 |
import java.util.List; |
9 |
import java.util.Map; |
|
10 | 10 |
|
11 | 11 |
/** |
12 | 12 |
* This is base interface for all AP detector. In case you need to implement new AP detector |
... | ... | |
57 | 57 |
* @param databaseConnection database connection |
58 | 58 |
* @return model class for results |
59 | 59 |
*/ |
60 |
QueryResultItem analyze(Project project, DatabaseConnection databaseConnection); |
|
60 |
QueryResultItem analyze(Project project, DatabaseConnection databaseConnection, Map<String, String> thresholds);
|
|
61 | 61 |
} |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/detectors/BusinessAsUsualDetectorImpl.java | ||
---|---|---|
28 | 28 |
// sql queries loaded from sql file |
29 | 29 |
private List<String> sqlQueries; |
30 | 30 |
|
31 |
private float getDivisionOfIterationsWithRetrospective() { |
|
31 |
private float getDivisionOfIterationsWithRetrospective(Map<String, String> thresholds) { |
|
32 |
if(thresholds != null) |
|
33 |
return new Percentage(Float.parseFloat(thresholds.get("divisionOfIterationsWithRetrospective"))).getValue(); |
|
34 |
|
|
32 | 35 |
return ((Percentage) antiPattern.getThresholds().get("divisionOfIterationsWithRetrospective").getValue()).getValue(); |
33 | 36 |
} |
34 | 37 |
|
35 |
private List<String> getSearchSubstringsWithRetrospective() { |
|
38 |
private List<String> getSearchSubstringsWithRetrospective(Map<String, String> thresholds) { |
|
39 |
if(thresholds != null) |
|
40 |
return Arrays.asList(thresholds.get("searchSubstringsWithRetrospective").split("\\|\\|")); |
|
41 |
|
|
36 | 42 |
return Arrays.asList(((String) antiPattern.getThresholds().get("searchSubstringsWithRetrospective").getValue()).split("\\|\\|")); |
37 | 43 |
} |
38 | 44 |
|
... | ... | |
78 | 84 |
* @return results of detection |
79 | 85 |
*/ |
80 | 86 |
@Override |
81 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection) { |
|
87 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection, Map<String, String> thresholds) {
|
|
82 | 88 |
|
83 | 89 |
// init values |
84 | 90 |
List<ResultDetail> resultDetails = new ArrayList<>(); |
... | ... | |
87 | 93 |
|
88 | 94 |
// go through the results of the queries and put in one map => in this map should be all iterations |
89 | 95 |
List<List<Map<String, Object>>> resultSets = databaseConnection.executeQueriesWithMultipleResults(project, |
90 |
Utils.fillQueryWithSearchSubstrings(this.sqlQueries, getSearchSubstringsWithRetrospective())); |
|
96 |
Utils.fillQueryWithSearchSubstrings(this.sqlQueries, getSearchSubstringsWithRetrospective(thresholds)));
|
|
91 | 97 |
for (int i = 0; i < resultSets.size(); i++) { |
92 | 98 |
List<Map<String, Object>> rs = resultSets.get(i); |
93 | 99 |
|
... | ... | |
113 | 119 |
|
114 | 120 |
} |
115 | 121 |
|
116 |
int minRetrospectiveLimit = Math.round(totalNumberIterations * getDivisionOfIterationsWithRetrospective()); |
|
122 |
int minRetrospectiveLimit = Math.round(totalNumberIterations * getDivisionOfIterationsWithRetrospective(thresholds));
|
|
117 | 123 |
|
118 | 124 |
resultDetails.add(new ResultDetail("Min retrospective limit", String.valueOf(minRetrospectiveLimit))); |
119 | 125 |
resultDetails.add(new ResultDetail("Found retrospectives", String.valueOf(iterationsResults.size()))); |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/detectors/LongOrNonExistentFeedbackLoopsDetectorImpl.java | ||
---|---|---|
35 | 35 |
// sql queries loaded from sql file |
36 | 36 |
private List<String> sqlQueries; |
37 | 37 |
|
38 |
private float getDivisionOfIterationsWithFeedbackLoop() { |
|
38 |
private float getDivisionOfIterationsWithFeedbackLoop(Map<String, String> thresholds) { |
|
39 |
if(thresholds != null) |
|
40 |
return new Percentage(Float.parseFloat(thresholds.get("divisionOfIterationsWithFeedbackLoop"))).getValue(); |
|
41 |
|
|
39 | 42 |
return ((Percentage) antiPattern.getThresholds().get("divisionOfIterationsWithFeedbackLoop").getValue()).getValue(); |
40 | 43 |
} |
41 | 44 |
|
42 |
private float getMaxGapBetweenFeedbackLoopRate() { |
|
45 |
private float getMaxGapBetweenFeedbackLoopRate(Map<String, String> thresholds) { |
|
46 |
if(thresholds != null) |
|
47 |
return new PositiveFloat(Float.parseFloat(thresholds.get("maxGapBetweenFeedbackLoopRate"))).floatValue(); |
|
48 |
|
|
43 | 49 |
return ((PositiveFloat) antiPattern.getThresholds().get("maxGapBetweenFeedbackLoopRate").getValue()).floatValue(); |
44 | 50 |
} |
45 | 51 |
|
46 |
private List<String> getSearchSubstringsWithFeedbackLoop() { |
|
52 |
private List<String> getSearchSubstringsWithFeedbackLoop(Map<String, String> thresholds) { |
|
53 |
if(thresholds != null) |
|
54 |
return Arrays.asList(thresholds.get("searchSubstringsWithFeedbackLoop").split("\\|\\|")); |
|
55 |
|
|
47 | 56 |
return Arrays.asList(((String) antiPattern.getThresholds().get("searchSubstringsWithFeedbackLoop").getValue()).split("\\|\\|")); |
48 | 57 |
} |
49 | 58 |
|
... | ... | |
92 | 101 |
* @return detection result |
93 | 102 |
*/ |
94 | 103 |
@Override |
95 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection) { |
|
104 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection, Map<String, String> thresholds) {
|
|
96 | 105 |
|
97 | 106 |
// init values |
98 | 107 |
long totalNumberIterations = 0; |
... | ... | |
105 | 114 |
Date projectEndDate = null; |
106 | 115 |
|
107 | 116 |
List<List<Map<String, Object>>> resultSets = databaseConnection.executeQueriesWithMultipleResults(project, |
108 |
Utils.fillQueryWithSearchSubstrings(this.sqlQueries, getSearchSubstringsWithFeedbackLoop())); |
|
117 |
Utils.fillQueryWithSearchSubstrings(this.sqlQueries, getSearchSubstringsWithFeedbackLoop(thresholds)));
|
|
109 | 118 |
for (int i = 0; i < resultSets.size(); i++) { |
110 | 119 |
List<Map<String, Object>> rs = resultSets.get(i); |
111 | 120 |
|
... | ... | |
147 | 156 |
} |
148 | 157 |
} |
149 | 158 |
|
150 |
double halfNumberOfIterations = totalNumberIterations * getDivisionOfIterationsWithFeedbackLoop(); |
|
159 |
double halfNumberOfIterations = totalNumberIterations * getDivisionOfIterationsWithFeedbackLoop(thresholds);
|
|
151 | 160 |
|
152 | 161 |
// if the number of iterations that contain at least one feedback activity is the ideal case |
153 | 162 |
if (totalNumberIterations <= numberOfIterationsWhichContainsAtLeastOneActivityForFeedback) { |
... | ... | |
170 | 179 |
long daysBetween = Utils.daysBetween(firstDate, secondDate); |
171 | 180 |
firstDate = secondDate; |
172 | 181 |
|
173 |
if (daysBetween >= getMaxGapBetweenFeedbackLoopRate() * averageIterationLength) { |
|
182 |
if (daysBetween >= getMaxGapBetweenFeedbackLoopRate(thresholds) * averageIterationLength) {
|
|
174 | 183 |
List<ResultDetail> resultDetails = Utils.createResultDetailsList( |
175 | 184 |
new ResultDetail("Days between", Long.toString(daysBetween)), |
176 | 185 |
new ResultDetail("Average iteration length", Integer.toString(averageIterationLength)), |
... | ... | |
211 | 220 |
long daysBetween = Utils.daysBetween(firstDate, secondDate); |
212 | 221 |
firstDate = secondDate; |
213 | 222 |
|
214 |
if (daysBetween >= getMaxGapBetweenFeedbackLoopRate() * averageIterationLength) { |
|
223 |
if (daysBetween >= getMaxGapBetweenFeedbackLoopRate(thresholds) * averageIterationLength) {
|
|
215 | 224 |
List<ResultDetail> resultDetails = Utils.createResultDetailsList( |
216 | 225 |
new ResultDetail("Days between", Long.toString(daysBetween)), |
217 | 226 |
new ResultDetail("Average iteration length", Integer.toString(averageIterationLength)), |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/detectors/NinetyNinetyRuleDetectorImpl.java | ||
---|---|---|
2 | 2 |
|
3 | 3 |
import cz.zcu.fav.kiv.antipatterndetectionapp.detecting.DatabaseConnection; |
4 | 4 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.*; |
5 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.types.Percentage; |
|
5 | 6 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.types.PositiveFloat; |
6 | 7 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.types.PositiveInteger; |
7 | 8 |
import cz.zcu.fav.kiv.antipatterndetectionapp.service.AntiPatternService; |
... | ... | |
26 | 27 |
// sql queries loaded from sql file |
27 | 28 |
private List<String> sqlQueries; |
28 | 29 |
|
29 |
private double getMaxDivisionRange() { |
|
30 |
private double getMaxDivisionRange(Map<String, String> thresholds) { |
|
31 |
if(thresholds != null) |
|
32 |
return new PositiveFloat(Float.parseFloat(thresholds.get("maxDivisionRange"))).floatValue(); |
|
33 |
|
|
30 | 34 |
return ((PositiveFloat) antiPattern.getThresholds().get("maxDivisionRange").getValue()).doubleValue(); |
31 | 35 |
} |
32 | 36 |
|
33 |
private int getMaxBadDivisionLimit() { |
|
37 |
private int getMaxBadDivisionLimit(Map<String, String> thresholds) { |
|
38 |
if(thresholds != null) |
|
39 |
return new PositiveInteger(Integer.parseInt(thresholds.get("maxBadDivisionLimit"))).intValue(); |
|
40 |
|
|
34 | 41 |
return ((PositiveInteger) antiPattern.getThresholds().get("maxBadDivisionLimit").getValue()).intValue(); |
35 | 42 |
} |
36 | 43 |
|
... | ... | |
72 | 79 |
* @return detection result |
73 | 80 |
*/ |
74 | 81 |
@Override |
75 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection) { |
|
82 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection, Map<String, String> thresholds) {
|
|
76 | 83 |
|
77 | 84 |
List<ResultDetail> resultDetails = new ArrayList<>(); |
78 | 85 |
List<Double> divisionsResults = new ArrayList<>(); |
... | ... | |
88 | 95 |
} |
89 | 96 |
divisionsResults.add(resultDivision); |
90 | 97 |
// if is one division is out of range set boolean to false |
91 |
if (resultDivision > getMaxDivisionRange()) { |
|
98 |
if (resultDivision > getMaxDivisionRange(thresholds)) {
|
|
92 | 99 |
isAllInRange = false; |
93 | 100 |
} |
94 | 101 |
} |
... | ... | |
102 | 109 |
|
103 | 110 |
int counterOverEstimated = 0; |
104 | 111 |
for (Double divisionResult : divisionsResults) { |
105 |
if (divisionResult > getMaxDivisionRange()) { |
|
112 |
if (divisionResult > getMaxDivisionRange(thresholds)) {
|
|
106 | 113 |
counterOverEstimated++; |
107 | 114 |
} else { |
108 | 115 |
counterOverEstimated = 0; |
109 | 116 |
} |
110 | 117 |
|
111 |
if (counterOverEstimated > getMaxBadDivisionLimit()) { |
|
118 |
if (counterOverEstimated > getMaxBadDivisionLimit(thresholds)) {
|
|
112 | 119 |
resultDetails.add(new ResultDetail("Conclusion", |
113 |
getMaxBadDivisionLimit() + " or more consecutive iterations has a bad trend in estimates")); |
|
120 |
getMaxBadDivisionLimit(thresholds) + " or more consecutive iterations has a bad trend in estimates"));
|
|
114 | 121 |
return new QueryResultItem(this.antiPattern, true, resultDetails); |
115 | 122 |
} |
116 | 123 |
|
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/detectors/RoadToNowhereDetectorImpl.java | ||
---|---|---|
2 | 2 |
|
3 | 3 |
import cz.zcu.fav.kiv.antipatterndetectionapp.detecting.DatabaseConnection; |
4 | 4 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.*; |
5 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.types.Percentage; |
|
6 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.types.PositiveFloat; |
|
5 | 7 |
import cz.zcu.fav.kiv.antipatterndetectionapp.model.types.PositiveInteger; |
6 | 8 |
import cz.zcu.fav.kiv.antipatterndetectionapp.service.AntiPatternService; |
7 | 9 |
import cz.zcu.fav.kiv.antipatterndetectionapp.service.AntiPatternServiceImpl; |
... | ... | |
14 | 16 |
import java.util.ArrayList; |
15 | 17 |
import java.util.Arrays; |
16 | 18 |
import java.util.List; |
19 |
import java.util.Map; |
|
17 | 20 |
|
18 | 21 |
public class RoadToNowhereDetectorImpl implements AntiPatternDetector { |
19 | 22 |
|
... | ... | |
34 | 37 |
// sql queries loaded from sql file |
35 | 38 |
private List<String> sqlQueries; |
36 | 39 |
|
37 |
private int getMinNumberOfWikiPagesWithProjectPlan() { |
|
40 |
private int getMinNumberOfWikiPagesWithProjectPlan(Map<String, String> thresholds) { |
|
41 |
if(thresholds != null) |
|
42 |
return new PositiveInteger(Integer.parseInt(thresholds.get("minNumberOfWikiPagesWithProjectPlan"))).intValue(); |
|
43 |
|
|
38 | 44 |
return ((PositiveInteger) antiPattern.getThresholds().get("minNumberOfWikiPagesWithProjectPlan").getValue()).intValue(); |
39 | 45 |
} |
40 | 46 |
|
41 |
private int getMinNumberOfActivitiesWithProjectPlan() { |
|
47 |
private int getMinNumberOfActivitiesWithProjectPlan(Map<String, String> thresholds) { |
|
48 |
if(thresholds != null) |
|
49 |
return new PositiveInteger(Integer.parseInt(thresholds.get("minNumberOfActivitiesWithProjectPlan"))).intValue(); |
|
50 |
|
|
42 | 51 |
return ((PositiveInteger) antiPattern.getThresholds().get("minNumberOfActivitiesWithProjectPlan").getValue()).intValue(); |
43 | 52 |
} |
44 | 53 |
|
45 |
private List<String> getSearchSubstringsWithProjectPlan() { |
|
54 |
private List<String> getSearchSubstringsWithProjectPlan(Map<String, String> thresholds) { |
|
55 |
if(thresholds != null) |
|
56 |
return Arrays.asList(thresholds.get("searchSubstringsWithProjectPlan").split("\\|\\|")); |
|
57 |
|
|
46 | 58 |
return Arrays.asList(((String) antiPattern.getThresholds().get("searchSubstringsWithProjectPlan").getValue()).split("\\|\\|")); |
47 | 59 |
} |
48 | 60 |
|
... | ... | |
82 | 94 |
* @return detection result |
83 | 95 |
*/ |
84 | 96 |
@Override |
85 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection) { |
|
97 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection, Map<String, String> thresholds) {
|
|
86 | 98 |
|
87 | 99 |
/* Init values */ |
88 | 100 |
List<ResultDetail> resultDetails = new ArrayList<>(); |
... | ... | |
91 | 103 |
|
92 | 104 |
try { |
93 | 105 |
ResultSet rs = databaseConnection.executeQueries(project, |
94 |
Utils.fillQueryWithSearchSubstrings(this.sqlQueries, getSearchSubstringsWithProjectPlan())); |
|
106 |
Utils.fillQueryWithSearchSubstrings(this.sqlQueries, getSearchSubstringsWithProjectPlan(thresholds)));
|
|
95 | 107 |
if (rs != null) { |
96 | 108 |
while (rs.next()) { |
97 | 109 |
numberOfIssuesForProjectPlan = rs.getInt("numberOfActivitiesWithSubstrings"); |
... | ... | |
108 | 120 |
resultDetails.add(new ResultDetail("Number of issues for creating project plan", String.valueOf(numberOfIssuesForProjectPlan))); |
109 | 121 |
resultDetails.add(new ResultDetail("Number of wiki pages for creating project plan", String.valueOf(numberOfWikiPagesForProjectPlan))); |
110 | 122 |
|
111 |
if (numberOfIssuesForProjectPlan >= getMinNumberOfActivitiesWithProjectPlan() || numberOfWikiPagesForProjectPlan >= getMinNumberOfWikiPagesWithProjectPlan()) {
|
|
123 |
if (numberOfIssuesForProjectPlan >= getMinNumberOfActivitiesWithProjectPlan(thresholds) || numberOfWikiPagesForProjectPlan >= getMinNumberOfWikiPagesWithProjectPlan(thresholds)) {
|
|
112 | 124 |
resultDetails.add(new ResultDetail("Conclusion", "Found some activities or wiki pages for project plan in first two iterations")); |
113 | 125 |
return new QueryResultItem(this.antiPattern, false, resultDetails); |
114 | 126 |
} else { |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/detectors/SpecifyNothingDetectorImpl.java | ||
---|---|---|
14 | 14 |
import java.util.ArrayList; |
15 | 15 |
import java.util.Arrays; |
16 | 16 |
import java.util.List; |
17 |
import java.util.Map; |
|
17 | 18 |
|
18 | 19 |
public class SpecifyNothingDetectorImpl implements AntiPatternDetector { |
19 | 20 |
|
... | ... | |
33 | 34 |
// sql queries loaded from sql file |
34 | 35 |
private List<String> sqlQueries; |
35 | 36 |
|
36 |
private int getMinNumberOfWikiPagesWithSpecification() { |
|
37 |
private int getMinNumberOfWikiPagesWithSpecification(Map<String, String> thresholds) { |
|
38 |
if(thresholds != null) |
|
39 |
return new PositiveInteger(Integer.parseInt(thresholds.get("minNumberOfWikiPagesWithSpecification"))).intValue(); |
|
40 |
|
|
37 | 41 |
return ((PositiveInteger) antiPattern.getThresholds().get("minNumberOfWikiPagesWithSpecification").getValue()).intValue(); |
38 | 42 |
} |
39 | 43 |
|
40 |
private int getMinNumberOfActivitiesWithSpecification() { |
|
44 |
private int getMinNumberOfActivitiesWithSpecification(Map<String, String> thresholds) { |
|
45 |
if(thresholds != null) |
|
46 |
return new PositiveInteger(Integer.parseInt(thresholds.get("minNumberOfActivitiesWithSpecification"))).intValue(); |
|
47 |
|
|
41 | 48 |
return ((PositiveInteger) antiPattern.getThresholds().get("minNumberOfActivitiesWithSpecification").getValue()).intValue(); |
42 | 49 |
} |
43 | 50 |
|
44 |
private int getMinAvgLengthOfActivityDescription() { |
|
51 |
private int getMinAvgLengthOfActivityDescription(Map<String, String> thresholds) { |
|
52 |
if(thresholds != null) |
|
53 |
return new PositiveInteger(Integer.parseInt(thresholds.get("minAvgLengthOfActivityDescription"))).intValue(); |
|
54 |
|
|
45 | 55 |
return ((PositiveInteger) antiPattern.getThresholds().get("minAvgLengthOfActivityDescription").getValue()).intValue(); |
46 | 56 |
} |
47 | 57 |
|
48 |
private List<String> getSearchSubstringsWithProjectSpecification() { |
|
58 |
private List<String> getSearchSubstringsWithProjectSpecification(Map<String, String> thresholds) { |
|
59 |
if(thresholds != null) |
|
60 |
return Arrays.asList(thresholds.get("searchSubstringsWithProjectSpecification").split("\\|\\|")); |
|
61 |
|
|
49 | 62 |
return Arrays.asList(((String) antiPattern.getThresholds().get("searchSubstringsWithProjectSpecification").getValue()).split("\\|\\|")); |
50 | 63 |
} |
51 | 64 |
|
... | ... | |
87 | 100 |
* @return detection result |
88 | 101 |
*/ |
89 | 102 |
@Override |
90 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection) { |
|
103 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection, Map<String, String> thresholds) {
|
|
91 | 104 |
|
92 | 105 |
/* Init values */ |
93 | 106 |
List<ResultDetail> resultDetails = new ArrayList<>(); |
... | ... | |
97 | 110 |
|
98 | 111 |
try { |
99 | 112 |
ResultSet rs = databaseConnection.executeQueries(project, |
100 |
Utils.fillQueryWithSearchSubstrings(this.sqlQueries, getSearchSubstringsWithProjectSpecification())); |
|
113 |
Utils.fillQueryWithSearchSubstrings(this.sqlQueries, getSearchSubstringsWithProjectSpecification(thresholds)));
|
|
101 | 114 |
if (rs != null) { |
102 | 115 |
while (rs.next()) { |
103 | 116 |
numberOfWikiPages = rs.getInt("numberOfWikiPagesWithSubstrings"); |
... | ... | |
115 | 128 |
resultDetails.add(new ResultDetail("Number of activities for specification", String.valueOf(numberOfActivitiesForSpecification))); |
116 | 129 |
resultDetails.add(new ResultDetail("Number of wiki pages for specification", String.valueOf(numberOfWikiPages))); |
117 | 130 |
|
118 |
if (numberOfActivitiesForSpecification >= getMinNumberOfActivitiesWithSpecification() || |
|
119 |
numberOfWikiPages >= getMinNumberOfWikiPagesWithSpecification()) { |
|
131 |
if (numberOfActivitiesForSpecification >= getMinNumberOfActivitiesWithSpecification(thresholds) ||
|
|
132 |
numberOfWikiPages >= getMinNumberOfWikiPagesWithSpecification(thresholds)) {
|
|
120 | 133 |
resultDetails.add(new ResultDetail("Conclusion", "Found activities or wiki pages that represents creation of specification")); |
121 | 134 |
return new QueryResultItem(this.antiPattern, false, resultDetails); |
122 | 135 |
} else { |
123 |
if (averageLengthOfIssueDescription > getMinAvgLengthOfActivityDescription()) { |
|
136 |
if (averageLengthOfIssueDescription > getMinAvgLengthOfActivityDescription(thresholds)) {
|
|
124 | 137 |
resultDetails.add(new ResultDetail("Conclusion", "Average length of activity description is grater then minimum")); |
125 | 138 |
return new QueryResultItem(this.antiPattern, false, resultDetails); |
126 | 139 |
} else { |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/detectors/TooLongSprintDetectorImpl.java | ||
---|---|---|
53 | 53 |
this.sqlQueries = queries; |
54 | 54 |
} |
55 | 55 |
|
56 |
private Integer getMaxIterationLength() { |
|
56 |
private Integer getMaxIterationLength(Map<String, String> thresholds) { |
|
57 |
if(thresholds != null) |
|
58 |
return new PositiveInteger(Integer.parseInt(thresholds.get("maxIterationLength"))).intValue(); |
|
59 |
|
|
57 | 60 |
return ((PositiveInteger) this.antiPattern.getThresholds().get("maxIterationLength").getValue()).intValue(); |
58 | 61 |
} |
59 | 62 |
|
60 |
private Integer getMaxNumberOfTooLongIterations() { |
|
63 |
private Integer getMaxNumberOfTooLongIterations(Map<String, String> thresholds) { |
|
64 |
if(thresholds != null) |
|
65 |
return new PositiveInteger(Integer.parseInt(thresholds.get("maxNumberOfTooLongIterations"))).intValue(); |
|
66 |
|
|
61 | 67 |
return ((PositiveInteger) this.antiPattern.getThresholds().get("maxNumberOfTooLongIterations").getValue()).intValue(); |
62 | 68 |
} |
63 | 69 |
|
... | ... | |
74 | 80 |
* @return detection result |
75 | 81 |
*/ |
76 | 82 |
@Override |
77 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection) { |
|
83 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection, Map<String, String> thresholds) {
|
|
78 | 84 |
|
79 | 85 |
// get configuration |
80 |
int maxIterationLength = getMaxIterationLength(); |
|
81 |
int maxNumberOfTooLongIterations = getMaxNumberOfTooLongIterations(); |
|
86 |
int maxIterationLength = getMaxIterationLength(thresholds);
|
|
87 |
int maxNumberOfTooLongIterations = getMaxNumberOfTooLongIterations(thresholds);
|
|
82 | 88 |
|
83 | 89 |
// auxiliary variables |
84 | 90 |
int numberOfLongIterations = 0; |
... | ... | |
106 | 112 |
resultDetails.add(new ResultDetail("Conclusion", "All iterations in limit")); |
107 | 113 |
} |
108 | 114 |
|
109 |
LOGGER.info(this.antiPattern.getPrintName()); |
|
110 |
LOGGER.info(resultDetails.toString()); |
|
111 |
|
|
112 | 115 |
return new QueryResultItem(this.antiPattern, numberOfLongIterations > maxNumberOfTooLongIterations, resultDetails); |
113 | 116 |
} |
114 | 117 |
} |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/detecting/detectors/VaryingSprintLengthDetectorImpl.java | ||
---|---|---|
13 | 13 |
import java.util.ArrayList; |
14 | 14 |
import java.util.Arrays; |
15 | 15 |
import java.util.List; |
16 |
import java.util.Map; |
|
16 | 17 |
|
17 | 18 |
public class VaryingSprintLengthDetectorImpl implements AntiPatternDetector { |
18 | 19 |
|
... | ... | |
31 | 32 |
// sql queries loaded from sql file |
32 | 33 |
private List<String> sqlQueries; |
33 | 34 |
|
34 |
private Integer getMaxDaysDifference() { |
|
35 |
private Integer getMaxDaysDifference(Map<String, String> thresholds) { |
|
36 |
if(thresholds != null) |
|
37 |
return new PositiveInteger(Integer.parseInt(thresholds.get("maxDaysDifference"))).intValue(); |
|
38 |
|
|
35 | 39 |
return ((PositiveInteger) this.antiPattern.getThresholds().get("maxDaysDifference").getValue()).intValue(); |
36 | 40 |
} |
37 | 41 |
|
38 |
private Integer getMaxIterationChanged() { |
|
42 |
private Integer getMaxIterationChanged(Map<String, String> thresholds) { |
|
43 |
if(thresholds != null) |
|
44 |
return new PositiveInteger(Integer.parseInt(thresholds.get("maxIterationChanged"))).intValue(); |
|
45 |
|
|
39 | 46 |
return ((PositiveInteger) this.antiPattern.getThresholds().get("maxIterationChanged").getValue()).intValue(); |
40 | 47 |
} |
41 | 48 |
|
... | ... | |
78 | 85 |
* @return detection result |
79 | 86 |
*/ |
80 | 87 |
@Override |
81 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection) { |
|
88 |
public QueryResultItem analyze(Project project, DatabaseConnection databaseConnection, Map<String, String> thresholds) {
|
|
82 | 89 |
|
83 | 90 |
// init values |
84 | 91 |
List<ResultDetail> resultDetails = new ArrayList<>(); |
... | ... | |
100 | 107 |
secondIterationLength = iterationLength; |
101 | 108 |
} |
102 | 109 |
|
103 |
if (Math.abs(firstIterationLength - secondIterationLength) >= getMaxDaysDifference()) { |
|
110 |
if (Math.abs(firstIterationLength - secondIterationLength) >= getMaxDaysDifference(thresholds)) {
|
|
104 | 111 |
iterationLengthChanged = iterationLengthChanged + 1; |
105 | 112 |
} |
106 | 113 |
firstIterationLength = secondIterationLength; |
... | ... | |
113 | 120 |
return new QueryResultItem(this.antiPattern, true, resultDetails); |
114 | 121 |
} |
115 | 122 |
|
116 |
resultDetails.add(new ResultDetail("Maximum iteration length change", String.valueOf(getMaxIterationChanged()))); |
|
123 |
resultDetails.add(new ResultDetail("Maximum iteration length change", String.valueOf(getMaxIterationChanged(thresholds))));
|
|
117 | 124 |
resultDetails.add(new ResultDetail("Count of iterations", String.valueOf(numberOfIterations))); |
118 | 125 |
resultDetails.add(new ResultDetail("Iteration length changed", String.valueOf(iterationLengthChanged))); |
119 | 126 |
|
120 | 127 |
|
121 |
if (iterationLengthChanged > getMaxIterationChanged()) { |
|
128 |
if (iterationLengthChanged > getMaxIterationChanged(thresholds)) {
|
|
122 | 129 |
resultDetails.add(new ResultDetail("Conclusion", "Iteration length changed significantly too often")); |
123 | 130 |
} else { |
124 | 131 |
resultDetails.add(new ResultDetail("Conclusion", "Varying iteration length is all right")); |
125 | 132 |
} |
126 | 133 |
|
127 |
LOGGER.info(this.antiPattern.getPrintName()); |
|
128 |
LOGGER.info(resultDetails.toString()); |
|
129 |
|
|
130 |
return new QueryResultItem(this.antiPattern, (iterationLengthChanged > getMaxIterationChanged()), resultDetails); |
|
134 |
return new QueryResultItem(this.antiPattern, (iterationLengthChanged > getMaxIterationChanged(thresholds)), resultDetails); |
|
131 | 135 |
} |
132 | 136 |
} |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/service/ConfigurationService.java | ||
---|---|---|
1 | 1 |
package cz.zcu.fav.kiv.antipatterndetectionapp.service; |
2 | 2 |
|
3 |
import java.util.HashMap; |
|
3 | 4 |
import java.util.List; |
5 |
import java.util.Map; |
|
4 | 6 |
|
5 | 7 |
public interface ConfigurationService { |
6 | 8 |
|
... | ... | |
8 | 10 |
|
9 | 11 |
List<String> getDefaultConfigurationNames(); |
10 | 12 |
|
11 |
|
|
13 |
Map<String, Map<String, String>> getConfigurationByName(String configurationName); |
|
12 | 14 |
} |
src/main/java/cz/zcu/fav/kiv/antipatterndetectionapp/service/ConfigurationServiceImpl.java | ||
---|---|---|
6 | 6 |
|
7 | 7 |
import java.util.ArrayList; |
8 | 8 |
import java.util.List; |
9 |
import java.util.Map; |
|
9 | 10 |
|
10 | 11 |
@Service |
11 | 12 |
public class ConfigurationServiceImpl implements ConfigurationService { |
... | ... | |
17 | 18 |
public List<String> getAllConfigurationNames() { |
18 | 19 |
List<String> configList = new ArrayList<String>(); |
19 | 20 |
|
20 |
// get all configurations |
|
21 |
// insert default configuration |
|
22 |
configList.add("Default"); |
|
23 |
|
|
24 |
// get all external configurations |
|
21 | 25 |
for (String key : configurationRepository.allConfigurations.keySet() ) { |
22 | 26 |
configList.add(key); |
23 | 27 |
} |
... | ... | |
35 | 39 |
return configList; |
36 | 40 |
} |
37 | 41 |
|
42 |
@Override |
|
43 |
public Map<String, Map<String, String>> getConfigurationByName(String configurationName) { |
|
44 |
return configurationRepository.allConfigurations.get(configurationName); |
|
45 |
} |
|
38 | 46 |
} |
src/main/webapp/configurations/Default.json | ||
---|---|---|
1 |
{ |
|
2 |
"configuration": [ |
|
3 |
{ |
|
4 |
"antiPattern": "TooLongSprint", |
|
5 |
"thresholds": [ |
|
6 |
{ |
|
7 |
"thresholdName": "maxIterationLength", |
|
8 |
"value": "21" |
|
9 |
}, |
|
10 |
{ |
|
11 |
"thresholdName": "maxNumberOfTooLongIterations", |
|
12 |
"value": "0" |
|
13 |
} |
|
14 |
] |
|
15 |
}, |
|
16 |
{ |
|
17 |
"antiPattern": "VaryingSprintLength", |
|
18 |
"thresholds": [ |
|
19 |
{ |
|
20 |
"thresholdName": "maxDaysDifference", |
|
21 |
"value": "7" |
|
22 |
}, |
|
23 |
{ |
|
24 |
"thresholdName": "maxIterationChanged", |
|
25 |
"value": "1" |
|
26 |
} |
|
27 |
] |
|
28 |
}, |
|
29 |
{ |
|
30 |
"antiPattern": "BusinessAsUsual", |
|
31 |
"thresholds": [ |
|
32 |
{ |
|
33 |
"thresholdName": "divisionOfIterationsWithRetrospective", |
|
34 |
"value": "66.66f" |
|
35 |
}, |
|
36 |
{ |
|
37 |
"thresholdName": "searchSubstringsWithRetrospective", |
|
38 |
"value": "%retr%||%revi%||%week%scrum%" |
|
39 |
} |
|
40 |
] |
|
41 |
}, |
|
42 |
{ |
|
43 |
"antiPattern": "SpecifyNothing", |
|
44 |
"thresholds": [ |
|
45 |
{ |
|
46 |
"thresholdName": "minNumberOfWikiPagesWithSpecification", |
|
47 |
"value": "1" |
|
48 |
}, |
|
49 |
{ |
|
50 |
"thresholdName": "minNumberOfActivitiesWithSpecification", |
|
51 |
"value": "1" |
|
52 |
}, |
|
53 |
{ |
|
54 |
"thresholdName": "minAvgLengthOfActivityDescription", |
|
55 |
"value": "150" |
|
56 |
}, |
|
57 |
{ |
|
58 |
"thresholdName": "searchSubstringsWithProjectSpecification", |
|
59 |
"value": "%dsp%||%specifikace%||%specification%||%vize%proj%||%vize%produ%" |
|
60 |
} |
|
61 |
] |
|
62 |
}, |
|
63 |
{ |
|
64 |
"antiPattern": "RoadToNowhere", |
|
65 |
"thresholds": [ |
|
66 |
{ |
|
67 |
"thresholdName": "minNumberOfWikiPagesWithProjectPlan", |
|
68 |
"value": "1" |
|
69 |
}, |
|
70 |
{ |
|
71 |
"thresholdName": "minNumberOfActivitiesWithProjectPlan", |
|
72 |
"value": "1" |
|
73 |
}, |
|
74 |
{ |
|
75 |
"thresholdName": "searchSubstringsWithProjectPlan", |
|
76 |
"value": "%plán projektu%||%project plan%||%plan project%||%projektový plán%" |
|
77 |
} |
|
78 |
] |
|
79 |
}, |
|
80 |
{ |
|
81 |
"antiPattern": "LongOrNonExistentFeedbackLoops", |
|
82 |
"thresholds": [ |
|
83 |
{ |
|
84 |
"thresholdName": "divisionOfIterationsWithFeedbackLoop", |
|
85 |
"value": "50.00f" |
|
86 |
}, |
|
87 |
{ |
|
88 |
"thresholdName": "maxGapBetweenFeedbackLoopRate", |
|
89 |
"value": "2f" |
|
90 |
}, |
|
91 |
{ |
|
92 |
"thresholdName": "searchSubstringsWithFeedbackLoop", |
|
93 |
"value": "%schůz%zákazník%||%předvedení%zákazník%||%zákazn%demo%||%schůz%zadavat%||%inform%schůz%||%zákazn%||%zadavatel%" |
|
94 |
} |
|
95 |
] |
|
96 |
}, |
|
97 |
{ |
|
98 |
"antiPattern": "NinetyNinetyRule", |
|
99 |
"thresholds": [ |
|
100 |
{ |
|
101 |
"thresholdName": "maxDivisionRange", |
|
102 |
"value": "1.25f" |
|
103 |
}, |
|
104 |
{ |
|
105 |
"thresholdName": "maxBadDivisionLimit", |
|
106 |
"value": "2" |
|
107 |
} |
|
108 |
] |
|
109 |
} |
|
110 |
] |
|
111 |
} |
src/main/webapp/configurations/MyConfig.json | ||
---|---|---|
1 |
{ |
|
2 |
"configuration": [ |
|
3 |
{ |
|
4 |
"antiPattern": "TooLongSprint", |
|
5 |
"thresholds": [ |
|
6 |
{ |
|
7 |
"thresholdName": "maxIterationLength", |
|
8 |
"value": "21" |
|
9 |
}, |
|
10 |
{ |
|
11 |
"thresholdName": "maxNumberOfTooLongIterations", |
|
12 |
"value": "0" |
|
13 |
} |
|
14 |
] |
|
15 |
}, |
|
16 |
{ |
|
17 |
"antiPattern": "VaryingSprintLength", |
|
18 |
"thresholds": [ |
|
19 |
{ |
|
20 |
"thresholdName": "maxDaysDifference", |
|
21 |
"value": "7" |
|
22 |
}, |
|
23 |
{ |
|
24 |
"thresholdName": "maxIterationChanged", |
|
25 |
"value": "1" |
|
26 |
} |
|
27 |
] |
|
28 |
}, |
|
29 |
{ |
|
30 |
"antiPattern": "BusinessAsUsual", |
|
31 |
"thresholds": [ |
|
32 |
{ |
|
33 |
"thresholdName": "divisionOfIterationsWithRetrospective", |
|
34 |
"value": "66.66f" |
|
35 |
}, |
|
36 |
{ |
|
37 |
"thresholdName": "searchSubstringsWithRetrospective", |
|
38 |
"value": "%retr%||%revi%||%week%scrum%" |
|
39 |
} |
|
40 |
] |
|
41 |
}, |
|
42 |
{ |
|
43 |
"antiPattern": "SpecifyNothing", |
|
44 |
"thresholds": [ |
|
45 |
{ |
|
46 |
"thresholdName": "minNumberOfWikiPagesWithSpecification", |
|
47 |
"value": "1" |
|
48 |
}, |
|
49 |
{ |
|
50 |
"thresholdName": "minNumberOfActivitiesWithSpecification", |
|
51 |
"value": "1" |
|
52 |
}, |
|
53 |
{ |
|
54 |
"thresholdName": "minAvgLengthOfActivityDescription", |
|
55 |
"value": "150" |
|
56 |
}, |
|
57 |
{ |
|
58 |
"thresholdName": "searchSubstringsWithProjectSpecification", |
|
59 |
"value": "%dsp%||%specifikace%||%specification%||%vize%proj%||%vize%produ%" |
|
60 |
} |
|
61 |
] |
|
62 |
}, |
|
63 |
{ |
|
64 |
"antiPattern": "RoadToNowhere", |
|
65 |
"thresholds": [ |
|
66 |
{ |
|
67 |
"thresholdName": "minNumberOfWikiPagesWithProjectPlan", |
|
68 |
"value": "1" |
|
69 |
}, |
|
70 |
{ |
|
71 |
"thresholdName": "minNumberOfActivitiesWithProjectPlan", |
|
72 |
"value": "1" |
|
73 |
}, |
|
74 |
{ |
|
75 |
"thresholdName": "searchSubstringsWithProjectPlan", |
|
76 |
"value": "%plán projektu%||%project plan%||%plan project%||%projektový plán%" |
|
77 |
} |
|
78 |
] |
|
79 |
}, |
|
80 |
{ |
|
81 |
"antiPattern": "LongOrNonExistentFeedbackLoops", |
|
82 |
"thresholds": [ |
|
83 |
{ |
|
84 |
"thresholdName": "divisionOfIterationsWithFeedbackLoop", |
|
85 |
"value": "50.00f" |
|
86 |
}, |
|
87 |
{ |
|
88 |
"thresholdName": "maxGapBetweenFeedbackLoopRate", |
|
89 |
"value": "2f" |
|
90 |
}, |
|
91 |
{ |
|
92 |
"thresholdName": "searchSubstringsWithFeedbackLoop", |
|
93 |
"value": "%schůz%zákazník%||%předvedení%zákazník%||%zákazn%demo%||%schůz%zadavat%||%inform%schůz%||%zákazn%||%zadavatel%" |
|
94 |
} |
|
95 |
] |
|
96 |
}, |
|
97 |
{ |
|
98 |
"antiPattern": "NinetyNinetyRule", |
|
99 |
"thresholds": [ |
|
100 |
{ |
|
101 |
"thresholdName": "maxDivisionRange", |
|
102 |
"value": "1.25f" |
|
103 |
}, |
|
104 |
{ |
|
105 |
"thresholdName": "maxBadDivisionLimit", |
|
106 |
"value": "2" |
|
107 |
} |
|
108 |
] |
|
109 |
} |
|
110 |
] |
|
111 |
} |
Také k dispozici: Unified diff
#14 Adding possibilty to use configurations from external files in analyzing