Commit 74cb5c92 authored by Johannes Eder's avatar Johannes Eder
Browse files

removed glossary occurences

Issue-Ref: 3886
Issue-Url: https://af3-developer.fortiss.org/issues/3886

Signed-off-by: Johannes Eder's avatarJohannes Eder <eder@fortiss.org>
parent 0e6f9fc7
......@@ -68,12 +68,6 @@
modelElementClass="org.fortiss.af3.mira.model.glossary.GlossaryEntry">
</modelElementClass>
</modelEditorBinding>
<modelEditorBinding
binding="org.fortiss.af3.mira.ui.editorbinding.GlossaryEntryEditorBindings$GlossaryEntrySecondaryEditorBinding">
<modelElementClass
modelElementClass="org.fortiss.af3.mira.model.glossary.GlossaryEntry">
</modelElementClass>
</modelEditorBinding>
<modelEditorBinding
binding="org.fortiss.af3.mira.ui.editorbinding.GlossaryEditorBinding">
<modelElementClass
......@@ -98,12 +92,6 @@
modelElementClass="org.fortiss.af3.mira.model.RequirementsPackage">
</modelElementClass>
</modelEditorBinding>
<modelEditorBinding
binding="org.fortiss.af3.mira.ui.editorbinding.GlossaryOccurrencesEditorBinding">
<modelElementClass
modelElementClass="org.fortiss.af3.mira.model.glossary.GlossaryEntry">
</modelElementClass>
</modelEditorBinding>
</extension>
<extension
point="org.eclipse.ui.views">
......
/*-------------------------------------------------------------------------+
| Copyright 2014 fortiss GmbH |
| |
| 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.fortiss.af3.mira.ui.editor.glossary;
import static org.apache.commons.lang.StringUtils.join;
import static org.fortiss.af3.mira.ui.utils.ContextUtils.getPluralEndings;
import static org.fortiss.af3.mira.ui.utils.ListenerUtils.openClassEditorListener;
import static org.fortiss.af3.mira.ui.utils.MiraLayoutUtils.singleLineLabelFactory;
import static org.fortiss.tooling.kernel.utils.EcoreUtils.pickInstanceOf;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.util.EContentAdapter;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Scale;
import org.eclipse.swt.widgets.Text;
import org.fortiss.af3.mira.model.Analysis;
import org.fortiss.af3.mira.model.ContextElement;
import org.fortiss.af3.mira.model.Requirement;
import org.fortiss.af3.mira.model.RequirementsContainer;
import org.fortiss.af3.mira.model.RequirementsPackage;
import org.fortiss.af3.mira.model.WordElement;
import org.fortiss.af3.mira.model.glossary.Glossary;
import org.fortiss.af3.mira.model.glossary.GlossaryEntry;
import org.fortiss.af3.mira.ui.editor.overview.OverviewEditorBase;
import org.fortiss.tooling.base.model.element.IHierarchicElement;
import org.fortiss.tooling.kernel.model.INamedCommentedElement;
/**
* Editor to list the occurrences of the Glossary in {@link Requirement}, {@link GlossaryEntry}
*
* @author candogan
*/
public class GlossaryOccurrencesEditor extends OverviewEditorBase<ContextElement> {
/** Number of {@link GlossaryEntry}s. */
protected int glossaryEntryNum;
/** Number of {@link Requirement}s. */
protected int requirementsEntryNum;
/** The final filtered list going to be shown on the Glossary Occurrences List. */
protected ArrayList<Object> filteredList;
/** The scale to determine the Levenshtein distance. */
private Scale scale;
/** The text of the Levenshtein scale. */
private Text scaleText;
/**
* The maximum length of the scale depending on the name of the glossary entry and it's
* synonyms. Used to prevent very short nonsense bulk lists.
*/
private int scaleMaxValue;
/**
* The overall maximum length allowed in the scale independent of the glossary entry's
* properties.
*/
private int scaleMaxValueAbsolute = 3;
/** Adapter for refresh when changing Levenshtein value. */
private EContentAdapter scaleAndListRefreshAdapter;
/**
* Map holding every relevant element and its Levenshtein distance to the edited glossary entry.
* Elements with distance above threshold are not contained here.
*/
private HashMap<Object, Integer> levenshteinDistanceMap = new HashMap<Object, Integer>();
/**
* Map holding every relevant element and its actual occurrence string in its text.
*/
private HashMap<Object, String> textMatchMap = new HashMap<Object, String>();
/** Class holding the Levenshtein algorithm. */
private Levenshtein lev = new Levenshtein();
/** Label for holding the statistics string. */
private Label statisticsLabel;
/** {@inheritDoc} */
@Override
protected boolean accept(EObject element) {
return element instanceof RequirementsPackage || element instanceof Requirement ||
element instanceof GlossaryEntry;
}
/** {@inheritDoc} */
@Override
protected void createOverviewTreeColumns(final TreeViewer listTreeViewer) {
createOverviewTreeColumn(listTreeViewer, 130, "Type", 0);
createOverviewTreeColumn(listTreeViewer, 90, "Status", 1);
createOverviewTreeColumn(listTreeViewer, 200, "Name", 2);
createOverviewTreeColumn(listTreeViewer, 300, "Definition", 3);
createOverviewTreeColumn(listTreeViewer, 300, "Comment", 4);
createOverviewTreeColumn(listTreeViewer, 130, "Text Match", 5);
// Filtering.
listTreeViewer.addFilter(searchFilter);
scale.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
listTreeViewer.refresh();
}
});
// Open glossary entry on double-click.
listTreeViewer.addDoubleClickListener(openClassEditorListener(EObject.class, null));
}
/** {@inheritDoc} */
@Override
protected Object[] getTreeElements() {
ArrayList<Object> endList = new ArrayList<Object>();
for(Object e : filteredList) {
// If object is in the map and it's Levenshtein distance is <= the current selection
// add it to the end list.
if(levenshteinDistanceMap.get(e) != null &&
levenshteinDistanceMap.get(e) <= scale.getSelection()) {
endList.add(e);
}
}
return endList.toArray();
}
/**
* Tree Elements are loaded into arrays checking for glossaries, requirements, requirement
* sources.
*/
private void loadTreeElements() {
// initiate values
glossaryEntryNum = 0;
requirementsEntryNum = 0;
updateScaleMaxValue();
filteredList = new ArrayList<Object>();
Object[] obj = editedObject.eContainer().eContainer().eContainer().eContents().toArray();
EList<Analysis> analysisNodes = pickInstanceOf(Analysis.class, Arrays.asList(obj));
// Elements from data dictionary.
ArrayList<Object> arrayList = new ArrayList<Object>();
ArrayList<Object> enumerationList = new ArrayList<Object>();
ArrayList<Object> structureList = new ArrayList<Object>();
// Elements from requirements analysis.
ArrayList<Glossary> glossaries = new ArrayList<Glossary>();
ArrayList<RequirementsContainer> reqContainers = new ArrayList<RequirementsContainer>();
for(Analysis analysis : analysisNodes) {
EList<IHierarchicElement> containedElements = analysis.getContainedElements();
glossaries.addAll(pickInstanceOf(Glossary.class, containedElements));
reqContainers.addAll(pickInstanceOf(RequirementsContainer.class, containedElements));
}
ArrayList<GlossaryEntry> glossaryEntryList = new ArrayList<GlossaryEntry>();
ArrayList<Requirement> requirementsList = new ArrayList<Requirement>();
ArrayList<Object> requirementSourceList = new ArrayList<Object>();
for(Glossary glossary : glossaries) {
glossaryEntryList.addAll(glossary.getGlossaryEntryList());
}
for(RequirementsContainer reqContainer : reqContainers) {
requirementsList.addAll(reqContainer.getRequirementsList());
}
for(Object array : arrayList) {
setMinLevenshteinDistanceFromElement(editedObject, array,
Math.min(scaleMaxValueAbsolute, scaleMaxValue));
}
for(Object enumeration : enumerationList) {
setMinLevenshteinDistanceFromElement(editedObject, enumeration,
Math.min(scaleMaxValueAbsolute, scaleMaxValue));
}
for(Object structure : structureList) {
setMinLevenshteinDistanceFromElement(editedObject, structure,
Math.min(scaleMaxValueAbsolute, scaleMaxValue));
}
for(GlossaryEntry glossaryEntry : glossaryEntryList) {
// the glossary entry itself is not supposed to be in the list
if(glossaryEntry.getId() == editedObject.getId())
continue;
setMinLevenshteinDistanceFromElement(editedObject, glossaryEntry,
Math.min(scaleMaxValueAbsolute, scaleMaxValue));
}
for(Requirement requirement : requirementsList) {
setMinLevenshteinDistanceFromElement(editedObject, requirement,
Math.min(scaleMaxValueAbsolute, scaleMaxValue));
}
for(Object requirementSource : requirementSourceList) {
setMinLevenshteinDistanceFromElement(editedObject, requirementSource,
Math.min(scaleMaxValueAbsolute, scaleMaxValue));
}
}
/** {@inheritDoc} */
@Override
protected void addOverviewList(List<String> overviewList, INamedCommentedElement element) {
overviewLabelProvider.addOverviewContentFor(overviewList, element);
}
/** {@inheritDoc} */
@Override
protected void createSections() {
loadTreeElements();
createStatisticsSection();
createSectionLevenhsteinScale();
createFilterSection();
createOverviewSection(new GlossaryOccurrencesLabelProvider(textMatchMap),
new GlossaryOccurrencesComparator(textMatchMap));
}
/**
* Creates the slider for the Levenshtein distance.
*/
private void createSectionLevenhsteinScale() {
Composite generalComposite =
createNewSection("Similarity", "Increase scale to broaden search");
generalComposite.setLayout(new GridLayout(1, true));
scale = new Scale(generalComposite, SWT.NONE);
scaleText = new Text(generalComposite, SWT.READ_ONLY);
scaleText.setLayoutData(new GridData(SWT.FILL, SWT.TOP, false, false));
// Adapter for registering a glossary entry name change and updating the scale.
scaleAndListRefreshAdapter = new EContentAdapter() {
/** {@inheritDoc} */
@Override
public void notifyChanged(Notification notification) {
super.notifyChanged(notification);
if(notification.getEventType() != Notification.REMOVING_ADAPTER) {
if(!scale.isDisposed()) {
Display.getDefault().asyncExec(() -> {
updateLevenshteinScale();
loadTreeElements();
statisticsLabel.setText(getStatisticsLabel());
});
}
}
}
};
editedObject.eAdapters().add(scaleAndListRefreshAdapter);
updateLevenshteinScale();
scale.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if(scale.getSelection() == 0) {
scaleText.setText("100% similarity");
} else {
// Prints the % similarity to the name of the glossary entry, it's synonyms and
// abbreviations are ignored.
int sim = (int)((1.0 -
(scale.getSelection() / (double)editedObject.getName().length())) *
100.00);
// An entry with a very short name and long synonym might show weird values
// of <30 or even 0. As this text is no exact value but rather a visual
// indication, a hard border is introduced here.
if(sim < 30)
sim = 30;
scaleText.setText("?????????" + sim + "% similarity");
}
}
});
}
/**
* Updates the Levenshtein scale.
*/
private void updateLevenshteinScale() {
updateScaleMaxValue();
// The maximum ranges from 1 to a certain number adjusted so that small words only get a
// small scale to prevent useless bulk entries.
scale.setMaximum(Math.max(Math.min(scaleMaxValueAbsolute, scaleMaxValue), 1));
scale.setMinimum(0);
scale.setPageIncrement(1);
scale.setSelection(0);
scale.setEnabled(Math.min(scaleMaxValueAbsolute, scaleMaxValue) > 0);
scaleText.setText("100% similarity");
}
/**
* Updates the scale max value.
*/
private void updateScaleMaxValue() {
scaleMaxValue = editedObject.getName().length();
for(WordElement synonym : editedObject.getSynonyms()) {
if(synonym.getText().length() > scaleMaxValue)
scaleMaxValue = synonym.getText().length();
}
if(scaleMaxValue > 1)
scaleMaxValue -= 2;
}
/** {@inheritDoc} */
@Override
protected void createStatisticsSection() {
Composite generalComposite = createNewSection("Usage Statistics", "This entry is used in");
generalComposite.setLayout(new GridLayout(3, false));
statisticsLabel =
toolkit.createLabel(generalComposite, getStatisticsLabel(), SWT.READ_ONLY);
singleLineLabelFactory.applyTo(statisticsLabel);
return;
}
/**
* Get the up-to-date statistics label.
*
* @return The statistics label string.
*/
private String getStatisticsLabel() {
String label = " ";
if(glossaryEntryNum > 0)
label += glossaryEntryNum + " glossary entries, ";
if(requirementsEntryNum > 0)
label += requirementsEntryNum + " requirement entries. ";
label += "\t";
return label;
}
/**
* Sets the minimum Levenshtein distance of a glossary entry's string representation to another
* element's string representation below a certain threshold, respecting the special chars and
* plural endings.
*
* @param contextElement
* The context element.
* @param element
* The element.
* @param threshold
*/
public void setMinLevenshteinDistanceFromElement(ContextElement contextElement, Object element,
int threshold) {
ArrayList<String> searchStrings = getSearchStrings(contextElement);
String elementString = "";
if(element instanceof INamedCommentedElement) {
elementString += " " + ((INamedCommentedElement)element).getName();
elementString += " " + ((INamedCommentedElement)element).getComment();
}
if(element instanceof ContextElement) {
String abbreviationsString = join(((ContextElement)element).getAbbreviations(), " ");
String synonymsString = join(((ContextElement)element).getSynonyms(), " ");
elementString += abbreviationsString + " " + synonymsString;
elementString += ((ContextElement)element).getDefinition();
} else if(element instanceof Requirement) {
elementString += " " + ((Requirement)element).getAuthor();
elementString += " " + ((Requirement)element).getDescription();
elementString += " " + ((Requirement)element).getRationale();
elementString += " " + ((Requirement)element).getRequirementName();
elementString += " " + ((Requirement)element).getSources();
elementString += " " + ((Requirement)element).getTodo();
}
elementString = elementString.toLowerCase();
if(elementString.isEmpty())
return;
int minLevenshteinDistance = -1;
for(String keyword : searchStrings) {
if(keyword.isEmpty())
continue;
int distanceTemp =
getMinLevenshteinDistanceFromText(keyword, elementString, threshold, element);
if(distanceTemp != -1 &&
(minLevenshteinDistance == -1 || minLevenshteinDistance > distanceTemp)) {
// If something in the search strings is very short, for example an abbreviation,
// only count it if distance is 0.
if((keyword.length() < 4 && distanceTemp == 0) || keyword.length() >= 4) {
minLevenshteinDistance = distanceTemp;
levenshteinDistanceMap.put(element, minLevenshteinDistance);
if(!filteredList.contains(element)) {
filteredList.add(element);
}
if(minLevenshteinDistance == 0) {
if(element instanceof GlossaryEntry)
glossaryEntryNum++;
}
}
}
}
}
/**
* Get all relevant attributes of a {@link GlossaryEntry} for the search: Abbreviations,
* synonyms and name.
*
* @param editedObject
* the {@link GlossaryEntry}
* @return search string
*
*/
private ArrayList<String> getSearchStrings(ContextElement editedObject) {
ArrayList<String> searchStrings = new ArrayList<String>();
searchStrings.add(editedObject.getName().toLowerCase());
for(WordElement word : editedObject.getSynonyms()) {
searchStrings.add(word.toString().toLowerCase());
}
for(WordElement word : editedObject.getAbbreviations()) {
searchStrings.add(word.toString().toLowerCase());
}
return searchStrings;
}
/**
* Finds the minimum Levenshtein distance of a keyword in a text below a certain threshold,
* respecting the special chars and plural endings.
*
* @param keyword
* the keyword string
* @param text
* the text string
* @param threshold
* the threshold
* @param element
* the element which is currently searched
* @return the minimum Levenshtein distance or -1 if above the threshold
*/
private int getMinLevenshteinDistanceFromText(String keyword, String text, int threshold,
Object element) {
// Replaces all non-letter and non-number characters with whitespaces.
text = text.replaceAll("[^a-zA-Z0-9]", " ");
keyword = keyword.replaceAll("[^a-zA-Z0-9]", " ");
String[] splittedText = text.split("\\s+");
String[] splittedKeywords = keyword.split("\\s+");
// Minimum distance in the whole text.
int minLevenshteinDistance = -1;
// Search every word in the text.
for(int i = 0; i < splittedText.length; i++) {
int distanceTemp1 = 0;
// Search every word in keyword.
for(int j = 0; j < splittedKeywords.length; j++) {
if(i + j > splittedText.length - 1) {
distanceTemp1 = -1;
break;
}
int distanceTemp2 =
lev.getDistance(splittedKeywords[j], splittedText[i + j], threshold);
// If we are at the last word in keyword, check for plural endings.
if(j == splittedKeywords.length - 1 && distanceTemp2 != 0) {
int distanceTemp3 = -1;
for(String pluralEnding : getPluralEndings()) {
if(!pluralEnding.isEmpty() && splittedText[i + j].endsWith(pluralEnding)) {
String cut = splittedText[i + j].substring(0,
splittedText[i + j].length() - pluralEnding.length());
distanceTemp3 = lev.getDistance(splittedKeywords[j], cut, threshold);
if(distanceTemp3 != -1 && distanceTemp3 < distanceTemp2)
distanceTemp2 = distanceTemp3;
}
}
}
if(distanceTemp2 == -1) {
distanceTemp1 = -1;
break;
}
distanceTemp1 += distanceTemp2;
}
if(distanceTemp1 != -1 && distanceTemp1 <= threshold &&
(minLevenshteinDistance == -1 || minLevenshteinDistance > distanceTemp1)) {
minLevenshteinDistance = distanceTemp1;
if(levenshteinDistanceMap.get(element) == null ||
levenshteinDistanceMap.get(element) > distanceTemp1) {
String minDistanceString = "";
for(int i2 = 0; i2 < splittedKeywords.length; i2++) {
if(splittedText[i + i2] != null)
minDistanceString += splittedText[i + i2] + " ";
}
textMatchMap.put(element, minDistanceString);
}
}
}
return minLevenshteinDistance;
}