diff --git a/src/example/org/deidentifier/arx/examples/Example18.java b/src/example/org/deidentifier/arx/examples/Example18.java index 03cfab802..03263aa36 100644 --- a/src/example/org/deidentifier/arx/examples/Example18.java +++ b/src/example/org/deidentifier/arx/examples/Example18.java @@ -22,15 +22,21 @@ import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.Map; import org.deidentifier.arx.DataType; import org.deidentifier.arx.aggregates.HierarchyBuilder; import org.deidentifier.arx.aggregates.HierarchyBuilder.Type; +import org.deidentifier.arx.aggregates.HierarchyBuilderDate; +import org.deidentifier.arx.aggregates.HierarchyBuilderDate.Granularity; import org.deidentifier.arx.aggregates.HierarchyBuilderGroupingBased.Level; import org.deidentifier.arx.aggregates.HierarchyBuilderIntervalBased; import org.deidentifier.arx.aggregates.HierarchyBuilderIntervalBased.Interval; import org.deidentifier.arx.aggregates.HierarchyBuilderIntervalBased.Range; +import org.deidentifier.arx.aggregates.HierarchyBuilderPriorityBased.Priority; import org.deidentifier.arx.aggregates.HierarchyBuilderOrderBased; +import org.deidentifier.arx.aggregates.HierarchyBuilderPriorityBased; import org.deidentifier.arx.aggregates.HierarchyBuilderRedactionBased; import org.deidentifier.arx.aggregates.HierarchyBuilderRedactionBased.Order; @@ -43,7 +49,7 @@ * @author Florian Kohlmayer */ public class Example18 extends Example { - + /** * Entry point. * @@ -55,39 +61,36 @@ public static void main(String[] args) { intervalBased(); orderBased(); ldlCholesterol(); - dates(); + date(); loadStore(); + priorityBased(); } - + /** - * Exemplifies the use of the order-based builder. + * Exemplifies the use of the date-based builder. */ - private static void dates() { - + private static void date() { + String stringDateFormat = "yyyy-MM-dd HH:mm"; DataType dateType = DataType.createDate(stringDateFormat); // Create the builder - HierarchyBuilderOrderBased builder = HierarchyBuilderOrderBased.create(dateType, false); - - // Define grouping fanouts - builder.getLevel(0).addGroup(10, dateType.createAggregate().createIntervalFunction()); - builder.getLevel(1).addGroup(2, dateType.createAggregate().createIntervalFunction()); - - // Alternatively - // builder.setAggregateFunction(AggregateFunction.INTERVAL(DataType.INTEGER)); - // builder.getLevel(0).addFanout(10); - // builder.getLevel(1).addFanout(2); + HierarchyBuilderDate builder = HierarchyBuilderDate.create(dateType); + + // Define grouping + builder.setGranularities(new Granularity[] {Granularity.WEEK_YEAR, + Granularity.QUARTER_YEAR, + Granularity.YEAR}); System.out.println("---------------------"); - System.out.println("ORDER-BASED DATE HIERARCHY"); + System.out.println("DATE HIERARCHY"); System.out.println("---------------------"); System.out.println(""); System.out.println("SPECIFICATION"); // Print specification - for (Level level : builder.getLevels()) { + for (Granularity level : builder.getGranularities()) { System.out.println(level); } @@ -101,7 +104,7 @@ private static void dates() { printArray(builder.build().getHierarchy()); System.out.println(""); } - + /** * Returns example data. * @@ -134,6 +137,7 @@ private static String[] getExampleDateData(String stringFormat){ result[i] = format.format(date.getTime()); } + result[result.length - 1] = DataType.NULL_VALUE; return result; } @@ -150,7 +154,7 @@ private static String[] getExampleLDLData() { } return result; } - + /** * Exemplifies the use of the interval-based builder. */ @@ -199,7 +203,7 @@ private static void intervalBased() { printArray(builder.build().getHierarchy()); System.out.println(""); } - + /** * Exemplifies the use of the interval-based builder for LDL cholesterol * in mmol/l. @@ -248,7 +252,7 @@ private static void ldlCholesterol() { printArray(builder.build().getHierarchy()); System.out.println(""); } - + /** * Shows how to load and store hierarchy specifications. */ @@ -298,11 +302,6 @@ private static void orderBased() { // Define grouping fanouts builder.getLevel(0).addGroup(10, DataType.INTEGER.createAggregate().createIntervalFunction()); builder.getLevel(1).addGroup(2, DataType.INTEGER.createAggregate().createIntervalFunction()); - - // Alternatively - // builder.setAggregateFunction(AggregateFunction.INTERVAL(DataType.INTEGER)); - // builder.getLevel(0).addFanout(10); - // builder.getLevel(1).addFanout(2); System.out.println("---------------------"); System.out.println("ORDER-BASED HIERARCHY"); @@ -326,6 +325,48 @@ private static void orderBased() { System.out.println(""); } + /** + * Exemplifies the use of the priority-based builder. + */ + private static void priorityBased() { + + System.out.println("-------------------------"); + System.out.println("PRIORITY-BASED HIERARCHY"); + System.out.println("-------------------------"); + + // Data + String[] data = new String[] {"Prio1", + "Prio2", + "Prio3", + "Prio4", + "Prio5", + "Prio6", + "Prio7", + "Prio8", + "Prio9", + "Prio10", + "Prio11"}; + + Map priorities = new HashMap(); + for (int i = 0; i < data.length; i++) { + priorities.put(data[i], -i); + } + + // Create the builder + HierarchyBuilderPriorityBased builder = HierarchyBuilderPriorityBased.create(priorities, Priority.HIGHEST_TO_LOWEST); + builder.setMaxLevels(3); + + // Print info about resulting groups + System.out.println("Resulting levels: "+Arrays.toString(builder.prepare(data))); + + System.out.println(""); + System.out.println("RESULT"); + + // Print resulting hierarchy + printArray(builder.build().getHierarchy()); + System.out.println(""); + } + /** * Exemplifies the use of the redaction-based builder. */ diff --git a/src/gui/org/deidentifier/arx/gui/Controller.java b/src/gui/org/deidentifier/arx/gui/Controller.java index f200f6c80..f59465fc9 100644 --- a/src/gui/org/deidentifier/arx/gui/Controller.java +++ b/src/gui/org/deidentifier/arx/gui/Controller.java @@ -57,6 +57,7 @@ import org.deidentifier.arx.DataType.DataTypeDescription; import org.deidentifier.arx.RowSet; import org.deidentifier.arx.aggregates.HierarchyBuilder; +import org.deidentifier.arx.aggregates.StatisticsFrequencyDistribution; import org.deidentifier.arx.exceptions.RollbackRequiredException; import org.deidentifier.arx.gui.model.Model; import org.deidentifier.arx.gui.model.ModelAnonymizationConfiguration; @@ -788,6 +789,12 @@ public void actionMenuEditCreateHierarchy() { .getStatistics() .getDistinctValues(index); + StatisticsFrequencyDistribution distribution = model.getInputConfig() + .getInput() + .getHandle() + .getStatistics() + .getFrequencyDistribution(index); + if (data.length == 1) { main.showInfoDialog(main.getShell(), Resources.getMessage("Controller.18"), //$NON-NLS-1$ @@ -797,7 +804,7 @@ public void actionMenuEditCreateHierarchy() { HierarchyBuilder builder = model.getInputConfig().getHierarchyBuilder(attr); @SuppressWarnings({ "unchecked", "rawtypes" }) - ARXWizard> wizard = new HierarchyWizard(this, attr, builder, type, model.getLocale(), data); + ARXWizard> wizard = new HierarchyWizard(this, attr, builder, type, model.getLocale(), data, distribution); if (wizard.open(main.getShell())) { HierarchyWizardResult result = wizard.getResult(); diff --git a/src/gui/org/deidentifier/arx/gui/resources/messages.properties b/src/gui/org/deidentifier/arx/gui/resources/messages.properties index 5a47d636b..f4735dede 100644 --- a/src/gui/org/deidentifier/arx/gui/resources/messages.properties +++ b/src/gui/org/deidentifier/arx/gui/resources/messages.properties @@ -261,12 +261,21 @@ HierarchyWizardPageRedaction.6=Mask characters left to right HierarchyWizardPageRedaction.7=Mask characters right to left HierarchyWizardPageRedaction.8=Characters HierarchyWizardPageRedaction.9=Padding character +HierarchyWizardPagePriority.0=Create a hierarchy by prioritizing values +HierarchyWizardPagePriority.1=Specify the parameters +HierarchyWizardPagePriority.2=Prioritization +HierarchyWizardPagePriority.3=Prioritize by natural order (lowest to highest) +HierarchyWizardPagePriority.4=Prioritize by natural order (highest to lowest) +HierarchyWizardPagePriority.5=Prioritize by frequency (lowest to highest) +HierarchyWizardPagePriority.6=Prioritize by frequency (highest to lowest) +HierarchyWizardPagePriority.7=Maximal number of levels HierarchyWizardPageType.0=Create a generalization hierarchy HierarchyWizardPageType.1=Specify the type of hierarchy HierarchyWizardPageType.2=Use intervals (for variables with ratio scale) HierarchyWizardPageType.3=Use ordering (e.g., for variables with ordinal scale) HierarchyWizardPageType.4=Use masking (e.g., for alphanumeric strings) HierarchyWizardPageType.5=Use dates (for dates) +HierarchyWizardPageType.6=Use priorities (e.g., by frequency) HierarchyWizardPageDate.0=Create a hierarchy for dates HierarchyWizardPageDate.1=Specify the parameters HierarchyWizardPageDate.2=Granularity diff --git a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizard.java b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizard.java index c99c06316..6e2e26c58 100644 --- a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizard.java +++ b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizard.java @@ -18,7 +18,9 @@ package org.deidentifier.arx.gui.view.impl.wizard; import java.util.Date; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import org.deidentifier.arx.AttributeType.Hierarchy; import org.deidentifier.arx.DataType; @@ -28,6 +30,7 @@ import org.deidentifier.arx.aggregates.HierarchyBuilder.Type; import org.deidentifier.arx.aggregates.HierarchyBuilderIntervalBased; import org.deidentifier.arx.aggregates.HierarchyBuilderOrderBased; +import org.deidentifier.arx.aggregates.StatisticsFrequencyDistribution; import org.deidentifier.arx.gui.Controller; import org.deidentifier.arx.gui.resources.Resources; import org.deidentifier.arx.gui.view.impl.wizard.ARXWizardDialog.ARXWizardButton; @@ -110,6 +113,9 @@ public static interface HierarchyWizardView { /** Var. */ private HierarchyWizardPageRedaction pageRedaction; + /** Var. */ + private HierarchyWizardPagePriority pagePriority; + /** Var. */ private HierarchyWizardPageFinal pageFinal; @@ -125,6 +131,7 @@ public static interface HierarchyWizardView { * @param datatype * @param locale * @param items + * @param distribution */ @SuppressWarnings("unchecked") public HierarchyWizard(final Controller controller, @@ -132,11 +139,20 @@ public HierarchyWizard(final Controller controller, final HierarchyBuilder builder, final DataType datatype, final Locale locale, - final String[] items) { + final String[] items, + final StatisticsFrequencyDistribution distribution) { super(new Point(800, 400)); + // Build frequency map + Map frequency = new HashMap<>(); + if (distribution != null) { + for (int i = 0; i < distribution.values.length; i++) { + frequency.put(distribution.values[i], (int)(distribution.frequency[i] * (double)distribution.count)); + } + } + // Store - this.model = new HierarchyWizardModel(datatype, locale, items); + this.model = new HierarchyWizardModel(datatype, locale, items, frequency); this.controller = controller; // Parse given builder, if needed @@ -146,7 +162,7 @@ public HierarchyWizard(final Controller controller, } } catch (Exception e){ /* Die silently, and recover*/ - this.model = new HierarchyWizardModel(datatype, locale, items); + this.model = new HierarchyWizardModel(datatype, locale, items, frequency); } // Initialize window @@ -192,7 +208,8 @@ public HierarchyWizard(final Controller controller, } pageOrder = new HierarchyWizardPageOrder(controller, this, model, pageFinal); pageRedaction = new HierarchyWizardPageRedaction(controller, this, model, pageFinal); - pageType = new HierarchyWizardPageType(this, model, pageIntervals, pageOrder, pageRedaction, pageDate); + pagePriority = new HierarchyWizardPagePriority(controller, this, model, pageFinal); + pageType = new HierarchyWizardPageType(this, model, pageIntervals, pageOrder, pageRedaction, pageDate, pagePriority); } @Override @@ -201,6 +218,7 @@ public void addPages() { addPage(pageType); addPage(pageOrder); addPage(pageRedaction); + addPage(pagePriority); addPage(pageFinal); if (pageIntervals != null) { addPage(pageIntervals); @@ -316,6 +334,12 @@ else if (builder.getType() == Type.DATE_BASED) { this.pageType.updatePage(); this.getContainer().showPage(pageRedaction); break; + case PRIORITY_BASED: + this.pagePriority.updatePage(); + this.model.setType(Type.PRIORITY_BASED); + this.pageType.updatePage(); + this.getContainer().showPage(pagePriority); + break; } } @@ -344,6 +368,8 @@ private void save(){ builder = model.getIntervalModel().getBuilder(true); } else if (getDialog().getCurrentPage() instanceof HierarchyWizardPageRedaction){ builder = model.getRedactionModel().getBuilder(true); + } else if (getDialog().getCurrentPage() instanceof HierarchyWizardPagePriority){ + builder = model.getPriorityModel().getBuilder(true); } // Save diff --git a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardModel.java b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardModel.java index 4b3c4d101..8c8f27ac2 100644 --- a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardModel.java +++ b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardModel.java @@ -23,6 +23,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Map; import org.deidentifier.arx.AttributeType.Hierarchy; import org.deidentifier.arx.DataType; @@ -33,6 +34,7 @@ import org.deidentifier.arx.aggregates.HierarchyBuilder.Type; import org.deidentifier.arx.aggregates.HierarchyBuilderIntervalBased; import org.deidentifier.arx.aggregates.HierarchyBuilderOrderBased; +import org.deidentifier.arx.aggregates.HierarchyBuilderPriorityBased; import org.deidentifier.arx.gui.resources.Resources; /** @@ -58,6 +60,9 @@ public class HierarchyWizardModel { /** Var. */ private HierarchyWizardModelDate dateModel; + /** Var. */ + private HierarchyWizardModelPriority priorityModel; + /** Var. */ private final DataType dataType; @@ -70,8 +75,9 @@ public class HierarchyWizardModel { * @param dataType * @param locale * @param data + * @param frequency */ - public HierarchyWizardModel(DataType dataType, Locale locale, String[] data){ + public HierarchyWizardModel(DataType dataType, Locale locale, String[] data, Map frequency){ // Store this.data = data; @@ -83,6 +89,7 @@ public HierarchyWizardModel(DataType dataType, Locale locale, String[] data){ // Create models orderModel = new HierarchyWizardModelOrder(dataType, locale, getOrderData()); redactionModel = new HierarchyWizardModelRedaction(dataType, data); + priorityModel = new HierarchyWizardModelPriority(dataType, data, frequency); if (dataType instanceof DataTypeWithRatioScale){ if (data.length > 1 || data[0] != DataType.NULL_VALUE) { @@ -126,6 +133,8 @@ public HierarchyBuilder getBuilder(boolean serializable) throws Exception { return (HierarchyBuilder)dateModel.getBuilder(serializable); } else if (type == Type.ORDER_BASED) { return orderModel.getBuilder(serializable); + } else if (type == Type.PRIORITY_BASED) { + return priorityModel.getBuilder(serializable); } else { throw new IllegalArgumentException(Resources.getMessage("HierarchyWizardModel.0")); //$NON-NLS-1$ } @@ -154,6 +163,8 @@ public Hierarchy getHierarchy() { return dateModel.getHierarchy(); } else if (type == Type.ORDER_BASED) { return orderModel.getHierarchy(); + } else if (type == Type.PRIORITY_BASED) { + return priorityModel.getHierarchy(); } else { throw new RuntimeException(Resources.getMessage("HierarchyWizardModel.1")); //$NON-NLS-1$ } @@ -195,6 +206,15 @@ public HierarchyWizardModelDate getDateModel() { return dateModel; } + /** + * Returns the model of the order-based builder. + * + * @return + */ + public HierarchyWizardModelPriority getPriorityModel() { + return priorityModel; + } + /** * Returns the type. * @@ -229,6 +249,11 @@ public void parse(HierarchyBuilder builder) throws IllegalArgumentException{ this.dateModel.parse((HierarchyBuilder) builder); this.type = Type.DATE_BASED; } + } else if (builder.getType() == Type.PRIORITY_BASED) { + if (priorityModel != null){ + this.priorityModel.parse((HierarchyBuilderPriorityBased) builder); + this.type = Type.PRIORITY_BASED; + } } else { throw new IllegalArgumentException(Resources.getMessage("HierarchyWizardModel.2")); //$NON-NLS-1$ } diff --git a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardModelPriority.java b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardModelPriority.java new file mode 100644 index 000000000..6ef86ea0d --- /dev/null +++ b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardModelPriority.java @@ -0,0 +1,208 @@ +/* + * ARX Data Anonymization Tool + * Copyright 2012 - 2022 Fabian Prasser and contributors + * + * 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.deidentifier.arx.gui.view.impl.wizard; + +import java.util.Map; + +import org.deidentifier.arx.DataType; +import org.deidentifier.arx.aggregates.HierarchyBuilder; +import org.deidentifier.arx.aggregates.HierarchyBuilderPriorityBased; +import org.deidentifier.arx.gui.resources.Resources; +import org.deidentifier.arx.gui.view.impl.wizard.HierarchyWizard.HierarchyWizardView; + +/** + * A model for priority-based builders. + * + * @author Fabian Prasser + * @param + */ +public class HierarchyWizardModelPriority extends HierarchyWizardModelAbstract { + + /** + * Priorities + * @author Fabian Prasser + */ + public static enum Priority { + FREQUENCY_LOWEST_TO_HIGHEST, + FREQUENCY_HIGHEST_TO_LOWEST, + ORDER_LOWEST_TO_HIGHEST, + ORDER_HIGHEST_TO_LOWEST, + } + + /** Frequency*/ + private Map frequency; + + /** Priority */ + private Priority priority = Priority.FREQUENCY_HIGHEST_TO_LOWEST; + + /** Max levels */ + private int maxLevels = 10; + + /** Data type */ + private DataType dataType; + + /** + * Creates a new instance. + * + * @param dataType + * @param data + * @param frequency + */ + public HierarchyWizardModelPriority(DataType dataType, + String[] data, + Map frequency) { + + // Super + super(data); + + // Store + this.dataType = dataType; + this.frequency = frequency; + + // Update + this.update(); + } + + @Override + public HierarchyBuilderPriorityBased getBuilder(boolean serializable) { + + if (priority == Priority.FREQUENCY_HIGHEST_TO_LOWEST) { + return HierarchyBuilderPriorityBased.create(frequency, + HierarchyBuilderPriorityBased.Priority.HIGHEST_TO_LOWEST, + maxLevels); + } else if (priority == Priority.FREQUENCY_LOWEST_TO_HIGHEST) { + return HierarchyBuilderPriorityBased.create(frequency, + HierarchyBuilderPriorityBased.Priority.LOWEST_TO_HIGHEST, + maxLevels); + } else if (priority == Priority.ORDER_HIGHEST_TO_LOWEST) { + return HierarchyBuilderPriorityBased.create(dataType, + HierarchyBuilderPriorityBased.Priority.HIGHEST_TO_LOWEST, + maxLevels); + } else if (priority == Priority.ORDER_LOWEST_TO_HIGHEST) { + return HierarchyBuilderPriorityBased.create(dataType, + HierarchyBuilderPriorityBased.Priority.LOWEST_TO_HIGHEST, + maxLevels); + } + throw new IllegalStateException("Invalid internal state"); + } + + /** + * Max levels + * @return + */ + public int getMaxLevels() { + return this.maxLevels; + } + + /** + * Returns the priority + * @return + */ + public Priority getPriority() { + return this.priority; + } + + @SuppressWarnings("unchecked") + @Override + public void parse(HierarchyBuilder _builder) { + + if (!(_builder instanceof HierarchyBuilderPriorityBased)) { + return; + } + HierarchyBuilderPriorityBased builder = ((HierarchyBuilderPriorityBased)_builder); + + if (builder.getDataType() != null && !builder.getDataType().equals(this.dataType)) { + return; + } + + this.maxLevels = ((HierarchyBuilderPriorityBased)_builder).getMaxLevels(); + if (builder.getPriorities() != null) { + this.frequency = builder.getPriorities(); + if (builder.getPriority() == HierarchyBuilderPriorityBased.Priority.HIGHEST_TO_LOWEST) { + this.priority = Priority.FREQUENCY_HIGHEST_TO_LOWEST; + } else { + this.priority = Priority.FREQUENCY_LOWEST_TO_HIGHEST; + } + } + if (builder.getDataType() != null) { + this.dataType = (DataType)builder.getDataType(); + if (builder.getPriority() == HierarchyBuilderPriorityBased.Priority.HIGHEST_TO_LOWEST) { + this.priority = Priority.ORDER_HIGHEST_TO_LOWEST; + } else { + this.priority = Priority.ORDER_LOWEST_TO_HIGHEST; + } + } + } + + /** + * Set max levels + * @param maxLevels + */ + public void setMaxLevels(int maxLevels) { + if (this.maxLevels != maxLevels) { + this.maxLevels = maxLevels; + this.update(); + } + } + + /** + * Sets the priority + * + * @param priority + */ + public void setPriority(Priority priority) { + if (priority != this.priority) { + this.priority = priority; + this.update(); + } + } + + @Override + public void updateUI(HierarchyWizardView sender) { + // Empty by design + } + + @Override + protected void build() { + + // Clear + super.hierarchy = null; + super.error = null; + super.groupsizes = null; + + // Check + if (data == null) return; + + // Prepare + HierarchyBuilderPriorityBased builder = getBuilder(false); + + try { + super.groupsizes = builder.prepare(data); + } catch(Exception e){ + super.error = Resources.getMessage("HierarchyWizardModelRedaction.0"); //$NON-NLS-1$ + return; + } + + try { + super.hierarchy = builder.build(); + } catch(Exception e){ + super.error = Resources.getMessage("HierarchyWizardModelRedaction.1"); //$NON-NLS-1$ + return; + } + } +} diff --git a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageDate.java b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageDate.java index 534c6d128..ca1841ba3 100644 --- a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageDate.java +++ b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageDate.java @@ -78,8 +78,10 @@ public class HierarchyWizardPageDate extends HierarchyWizardPageBuilder { /** State */ private boolean formatOK = false; + /** State */ private boolean topCodingOK = true; + /** State */ private boolean bottomCodingOK = true; diff --git a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPagePriority.java b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPagePriority.java new file mode 100644 index 000000000..44777c18f --- /dev/null +++ b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPagePriority.java @@ -0,0 +1,215 @@ +/* + * ARX Data Anonymization Tool + * Copyright 2012 - 2022 Fabian Prasser and contributors + * + * 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.deidentifier.arx.gui.view.impl.wizard; + +import org.deidentifier.arx.gui.Controller; +import org.deidentifier.arx.gui.resources.Resources; +import org.deidentifier.arx.gui.view.SWTUtil; +import org.deidentifier.arx.gui.view.impl.wizard.HierarchyWizardModelPriority.Priority; +import org.eclipse.jface.fieldassist.ControlDecoration; +import org.eclipse.jface.fieldassist.FieldDecorationRegistry; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyEvent; +import org.eclipse.swt.events.ModifyListener; +import org.eclipse.swt.events.SelectionAdapter; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; + +/** + * A page for configuring the priority-based builder. + * + * @author Fabian Prasser + * @param + */ +public class HierarchyWizardPagePriority extends HierarchyWizardPageBuilder { + + /** Var. */ + private final HierarchyWizardModelPriority model; + + /** Var. */ + private Button buttonOrderLowestToHighest; + + /** Var. */ + private Button buttonOrderHighestToLowest; + + /** Var. */ + private Button buttonFrequencyLowestToHighest; + + /** Var. */ + private Button buttonFrequencyHighestToLowest; + + /** Var. */ + private Text textMaxLevel; + + /** + * Creates a new instance. + * + * @param controller + * @param wizard + * @param model + * @param finalPage + */ + public HierarchyWizardPagePriority(Controller controller, + final HierarchyWizard wizard, + final HierarchyWizardModel model, + final HierarchyWizardPageFinal finalPage) { + super(wizard, model.getPriorityModel(), finalPage); + this.model = model.getPriorityModel(); + setTitle(Resources.getMessage("HierarchyWizardPagePriority.0")); //$NON-NLS-1$ + setDescription(Resources.getMessage("HierarchyWizardPagePriority.1")); //$NON-NLS-1$ + setPageComplete(true); + } + + @Override + public void createControl(final Composite parent) { + + // Base + Composite composite = new Composite(parent, SWT.NONE); + composite.setLayout(SWTUtil.createGridLayout(1, false)); + + // Frame + Group group1 = new Group(composite, SWT.SHADOW_ETCHED_IN); + group1.setText(Resources.getMessage("HierarchyWizardPagePriority.2")); //$NON-NLS-1$ + group1.setLayout(SWTUtil.createGridLayout(1, false)); + group1.setLayoutData(SWTUtil.createFillHorizontallyGridData()); + + // Max levels + Label label5 = new Label(group1, SWT.NONE); + label5.setText(Resources.getMessage("HierarchyWizardPagePriority.7")); //$NON-NLS-1$ + textMaxLevel = new Text(group1, SWT.BORDER); + textMaxLevel.setLayoutData(SWTUtil.createFillHorizontallyGridData()); + decorate(textMaxLevel); + + // Options + buttonOrderLowestToHighest = new Button(group1, SWT.RADIO); + buttonOrderLowestToHighest.setText(Resources.getMessage("HierarchyWizardPagePriority.3")); //$NON-NLS-1$ + buttonOrderHighestToLowest = new Button(group1, SWT.RADIO); + buttonOrderHighestToLowest.setText(Resources.getMessage("HierarchyWizardPagePriority.4")); //$NON-NLS-1$ + buttonFrequencyLowestToHighest = new Button(group1, SWT.RADIO); + buttonFrequencyLowestToHighest.setText(Resources.getMessage("HierarchyWizardPagePriority.5")); //$NON-NLS-1$ + buttonFrequencyHighestToLowest = new Button(group1, SWT.RADIO); + buttonFrequencyHighestToLowest.setText(Resources.getMessage("HierarchyWizardPagePriority.6")); //$NON-NLS-1$ + + // Events + buttonOrderLowestToHighest.addSelectionListener(new SelectionAdapter(){ + @Override public void widgetSelected(SelectionEvent arg0) { + if (buttonOrderLowestToHighest.getSelection()) { + model.setPriority(Priority.ORDER_LOWEST_TO_HIGHEST); + } + } + }); + buttonOrderHighestToLowest.addSelectionListener(new SelectionAdapter(){ + @Override public void widgetSelected(SelectionEvent arg0) { + if (buttonOrderHighestToLowest.getSelection()) { + model.setPriority(Priority.ORDER_HIGHEST_TO_LOWEST); + } + } + }); + buttonFrequencyLowestToHighest.addSelectionListener(new SelectionAdapter(){ + @Override public void widgetSelected(SelectionEvent arg0) { + if (buttonFrequencyLowestToHighest.getSelection()) { + model.setPriority(Priority.FREQUENCY_LOWEST_TO_HIGHEST); + } + } + }); + buttonFrequencyHighestToLowest.addSelectionListener(new SelectionAdapter(){ + @Override public void widgetSelected(SelectionEvent arg0) { + if (buttonFrequencyHighestToLowest.getSelection()) { + model.setPriority(Priority.FREQUENCY_HIGHEST_TO_LOWEST); + } + } + }); + + updatePage(); + setControl(composite); + } + + @Override + public boolean isPageComplete() { + if (textMaxLevel.getText().length() == 0 || !isValidNumber(textMaxLevel.getText())) { + return false; + } else { + return true; + } + } + + @Override + public void setVisible(boolean value){ + super.setVisible(value); + model.setVisible(value); + } + + @Override + public void updatePage() { + buttonOrderLowestToHighest.setSelection(model.getPriority() == Priority.ORDER_LOWEST_TO_HIGHEST); + buttonOrderHighestToLowest.setSelection(model.getPriority() == Priority.ORDER_HIGHEST_TO_LOWEST); + buttonFrequencyLowestToHighest.setSelection(model.getPriority() == Priority.FREQUENCY_LOWEST_TO_HIGHEST); + buttonFrequencyHighestToLowest.setSelection(model.getPriority() == Priority.FREQUENCY_HIGHEST_TO_LOWEST); + textMaxLevel.setText(String.valueOf(model.getMaxLevels())); + } + + /** + * Decorates a text field for domain properties. + * + * @param text + */ + private void decorate(final Text text) { + final ControlDecoration decoration = new ControlDecoration(text, SWT.RIGHT); + text.addModifyListener(new ModifyListener(){ + @Override + public void modifyText(ModifyEvent arg0) { + if (!isValidNumber(text.getText())) { + decoration.setDescriptionText(Resources.getMessage("HierarchyWizardPageRedaction.23")); //$NON-NLS-1$ + Image image = FieldDecorationRegistry.getDefault() + .getFieldDecoration(FieldDecorationRegistry.DEC_ERROR) + .getImage(); + decoration.setImage(image); + decoration.show(); + } else { + decoration.hide(); + model.setMaxLevels(text.getText().length() == 0 ? 10 : Integer.valueOf(text.getText())); + } + setPageComplete(isPageComplete()); + } + }); + } + + /** + * Returns whether a valid number has been entered. + * + * @param text + * @return + */ + private boolean isValidNumber(String text) { + if (text.length() == 0) { + return true; + } else { + try { + int value = Integer.parseInt(text); + return value > 0d; + } catch (Exception e){ + return false; + } + } + } +} diff --git a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageRedaction.java b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageRedaction.java index 77995d637..b02492295 100644 --- a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageRedaction.java +++ b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageRedaction.java @@ -305,7 +305,6 @@ public void modifyText(ModifyEvent arg0) { }); } - /** * Returns the index of the item, or adds it to the combo. * diff --git a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageType.java b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageType.java index 56d90a84a..ff316da02 100644 --- a/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageType.java +++ b/src/gui/org/deidentifier/arx/gui/view/impl/wizard/HierarchyWizardPageType.java @@ -51,6 +51,9 @@ public class HierarchyWizardPageType extends WizardPage { /** Var. */ private Button date; + /** Var. */ + private Button priority; + /** Var. */ private IWizardPage next; @@ -66,6 +69,9 @@ public class HierarchyWizardPageType extends WizardPage { /** Var. */ private final HierarchyWizardPageDate datePage; + /** Var. */ + private final HierarchyWizardPagePriority priorityPage; + /** Var. */ private final HierarchyWizard wizard; @@ -78,13 +84,15 @@ public class HierarchyWizardPageType extends WizardPage { * @param orderPage * @param redactionPage * @param datePage + * @param priorityPage */ public HierarchyWizardPageType(final HierarchyWizard wizard, final HierarchyWizardModel model, final HierarchyWizardPageIntervals intervalPage, final HierarchyWizardPageOrder orderPage, final HierarchyWizardPageRedaction redactionPage, - final HierarchyWizardPageDate datePage) { + final HierarchyWizardPageDate datePage, + final HierarchyWizardPagePriority priorityPage) { super(""); //$NON-NLS-1$ this.wizard = wizard; @@ -92,6 +100,7 @@ public HierarchyWizardPageType(final HierarchyWizard wizard, this.orderPage = orderPage; this.intervalPage = intervalPage; this.datePage = datePage; + this.priorityPage = priorityPage; this.model = model; this.next = intervalPage; setTitle(Resources.getMessage("HierarchyWizardPageType.0")); //$NON-NLS-1$ @@ -126,6 +135,10 @@ public void createControl(final Composite parent) { this.redaction.setText(Resources.getMessage("HierarchyWizardPageType.4")); //$NON-NLS-1$ this.redaction.setEnabled(true); + this.priority = new Button(composite, SWT.RADIO); + this.priority.setText(Resources.getMessage("HierarchyWizardPageType.6")); //$NON-NLS-1$ + this.priority.setEnabled(true); + this.date.addSelectionListener(new SelectionAdapter(){ @Override public void widgetSelected(SelectionEvent arg0) { if (date.getSelection()) { @@ -161,17 +174,28 @@ public void createControl(final Composite parent) { } } }); + + this.priority.addSelectionListener(new SelectionAdapter(){ + @Override public void widgetSelected(SelectionEvent arg0) { + if (priority.getSelection()) { + next = priorityPage; + model.setType(Type.PRIORITY_BASED); + } + } + }); date.setSelection(model.getType() == Type.DATE_BASED); interval.setSelection(model.getType() == Type.INTERVAL_BASED); order.setSelection(model.getType() == Type.ORDER_BASED); redaction.setSelection(model.getType() == Type.REDACTION_BASED); + priority.setSelection(model.getType() == Type.PRIORITY_BASED); switch (model.getType()){ case DATE_BASED: next = datePage; break; case INTERVAL_BASED: next = intervalPage; break; case ORDER_BASED: next = orderPage; break; case REDACTION_BASED: next = redactionPage; break; + case PRIORITY_BASED: next = priorityPage; break; default: throw new IllegalStateException("Unknown type of builder"); //$NON-NLS-1$ } @@ -209,11 +233,13 @@ public void updatePage() { order.setSelection(model.getType() == Type.ORDER_BASED); redaction.setSelection(model.getType() == Type.REDACTION_BASED); date.setSelection(model.getType() == Type.DATE_BASED); + priority.setSelection(model.getType() == Type.PRIORITY_BASED); switch (model.getType()){ case INTERVAL_BASED: next = intervalPage; break; case ORDER_BASED: next = orderPage; break; case REDACTION_BASED: next = redactionPage; break; case DATE_BASED: next = datePage; break; + case PRIORITY_BASED: next = priorityPage; break; } } } diff --git a/src/main/org/deidentifier/arx/aggregates/HierarchyBuilder.java b/src/main/org/deidentifier/arx/aggregates/HierarchyBuilder.java index 66e55df5c..21b050691 100644 --- a/src/main/org/deidentifier/arx/aggregates/HierarchyBuilder.java +++ b/src/main/org/deidentifier/arx/aggregates/HierarchyBuilder.java @@ -53,7 +53,10 @@ public static enum Type { REDACTION_BASED("Redaction"), /** Date-based hierarchy */ - DATE_BASED("Date"); + DATE_BASED("Date"), + + /** Priority-based hierarchy */ + PRIORITY_BASED("Priority"); /** Name*/ private final String name; diff --git a/src/main/org/deidentifier/arx/aggregates/HierarchyBuilderDate.java b/src/main/org/deidentifier/arx/aggregates/HierarchyBuilderDate.java index a547f2e60..e5c4763bb 100644 --- a/src/main/org/deidentifier/arx/aggregates/HierarchyBuilderDate.java +++ b/src/main/org/deidentifier/arx/aggregates/HierarchyBuilderDate.java @@ -253,7 +253,7 @@ public boolean isFormatSupported() { * @param granularities * @return */ - public static HierarchyBuilder create(DataType type, Granularity... granularities){ + public static HierarchyBuilderDate create(DataType type, Granularity... granularities){ return create(type, null, new Format(), granularities); } @@ -265,7 +265,7 @@ public static HierarchyBuilder create(DataType type, Granularity... * @param granularities * @return */ - public static HierarchyBuilder create(DataType type, + public static HierarchyBuilderDate create(DataType type, TimeZone timeZone, Format format, Granularity... granularities){ @@ -282,7 +282,7 @@ public static HierarchyBuilder create(DataType type, * @param granularities * @return */ - public static HierarchyBuilder create(DataType type, + public static HierarchyBuilderDate create(DataType type, TimeZone timeZone, Format format, Date bottomCoding, @@ -300,7 +300,7 @@ public static HierarchyBuilder create(DataType type, * @throws IOException */ @SuppressWarnings("unchecked") - public static HierarchyBuilder create(File file) throws IOException{ + public static HierarchyBuilderDate create(File file) throws IOException{ ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream(file)); diff --git a/src/main/org/deidentifier/arx/aggregates/HierarchyBuilderPriorityBased.java b/src/main/org/deidentifier/arx/aggregates/HierarchyBuilderPriorityBased.java new file mode 100644 index 000000000..f6418644d --- /dev/null +++ b/src/main/org/deidentifier/arx/aggregates/HierarchyBuilderPriorityBased.java @@ -0,0 +1,398 @@ +/* + * ARX Data Anonymization Tool + * Copyright 2012 - 2022 Fabian Prasser and contributors + * + * 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.deidentifier.arx.aggregates; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.deidentifier.arx.AttributeType.Hierarchy; +import org.deidentifier.arx.DataType; + +/** + * This class enables building hierarchies mostly for categorical variables + * by iteratively removing the value with lowest priority + * + * @author Fabian Prasser + * @param + */ +public class HierarchyBuilderPriorityBased extends HierarchyBuilder implements Serializable { + + /** + * For priorities + * @author Fabian Prasser + */ + public static enum Priority { + /** Highest to lowest*/ + HIGHEST_TO_LOWEST, + /** Lowest to highest*/ + LOWEST_TO_HIGHEST + } + + /** SVUID */ + private static final long serialVersionUID = 4823242603368546852L; + + /** + * Create a new instance, prioritized by the order implied by the data type in + * order highest to lowest and a maximum of 10 levels. + * + * @param dataType + * @param + * @return + */ + public static HierarchyBuilderPriorityBased create(DataType type){ + return create(type, Priority.HIGHEST_TO_LOWEST); + } + + /** + * Create a new instance, prioritized by the order implied by the data type + * and a maximum of 10 levels. + * + * @param dataType + * @param priority + * @param + * @return + */ + public static HierarchyBuilderPriorityBased create(DataType type, Priority priority){ + return create(type, priority, 10); + } + + /** + * Create a new instance, prioritized by the order implied by the data type. + * + * @param maxLevels + * @param dataType + * @param + * @return + */ + public static HierarchyBuilderPriorityBased create(DataType type, Priority priority, int maxLevels){ + return new HierarchyBuilderPriorityBased(type, priority, maxLevels); + } + + /** + * Loads a builder specification from the given file. + * + * @param + * @param file + * @return + * @throws IOException + */ + @SuppressWarnings("unchecked") + public static HierarchyBuilderPriorityBased create(File file) throws IOException{ + ObjectInputStream ois = null; + try { + ois = new ObjectInputStream(new FileInputStream(file)); + HierarchyBuilderPriorityBased result = (HierarchyBuilderPriorityBased)ois.readObject(); + return result; + } catch (Exception e) { + throw new IOException(e); + } finally { + if (ois != null) ois.close(); + } + } + + + /** + * Create a new instance, prioritized by the order provided in the given array + * (highest to lowest) with a maximum of 10 levels. + * + * @param priorities + * @param + * @return + */ + public static HierarchyBuilderPriorityBased create(Map priorities){ + return create(priorities, Priority.HIGHEST_TO_LOWEST, 10); + } + + /** + * Create a new instance, prioritized by the order provided in the given array + * with the given order with a maximum of 10 levels. + * + * @param priorities + * @param priority + * @param + * @return + */ + public static HierarchyBuilderPriorityBased create(Map priorities, Priority priority){ + return create(priorities, priority, 10); + } + + /** + * Create a new instance, prioritized by the order provided in the given array + * (highest to lowest) + * + * @param priorities + * @param priority + * @param maxLevels + * @param + * @return + */ + public static HierarchyBuilderPriorityBased create(Map priorities, Priority priority, int maxLevels){ + return new HierarchyBuilderPriorityBased(priorities, priority, maxLevels); + } + + /** + * Loads a builder specification from the given file. + * + * @param + * @param file + * @return + * @throws IOException + */ + public static HierarchyBuilderPriorityBased create(String file) throws IOException{ + return create(new File(file)); + } + + + /** Result */ + private transient String[][] result; + + /** Max levels */ + private int maxLevels = 10; + + /** Priority */ + private Priority priority = null; + + /** Type */ + private DataType type = null; + + /** Priorities */ + private Map priorities = null; + + /** + * Creates a new instance + * @param type + * @param priority + * @param maxLevels + */ + private HierarchyBuilderPriorityBased(DataType type, Priority priority, int maxLevels) { + super(Type.PRIORITY_BASED); + this.maxLevels = maxLevels; + this.type = type; + this.priority = priority; + this.priorities = null; + } + + /** + * Creates a new instance + * @param priorities + * @param priority + * @param maxLevels + */ + private HierarchyBuilderPriorityBased(Map priorities, Priority priority, int maxLevels) { + super(Type.PRIORITY_BASED); + this.maxLevels = maxLevels; + this.priorities = priorities; + this.priority = priority; + this.type = null; + } + + /** + * Creates a new hierarchy, based on the predefined specification. + * + * @return + */ + public Hierarchy build(){ + + // Check + if (result == null) { + throw new IllegalArgumentException("Please call prepare() first"); + } + + // Return + Hierarchy h = Hierarchy.create(result); + this.result = null; + return h; + } + + /** + * Creates a new hierarchy, based on the predefined specification. + * + * @param data - Prioritized from highest to lowest + * @return + */ + public Hierarchy build(String[] data){ + prepare(data); + return build(); + } + + /** + * @return the type + */ + public DataType getDataType() { + return type; + } + + /** + * Gets the maximal number of levels + * @return the maxLevels + */ + public int getMaxLevels() { + return maxLevels; + } + + /** + * @return the priorities + */ + public Map getPriorities() { + return priorities; + } + + /** + * @return the priority + */ + public Priority getPriority() { + return priority; + } + + /** + * Returns whether domain-properties are available for this builder. Currently, this information is only used for + * evaluating information loss with the generalized loss metric for attributes with functional + * redaction-based hierarchies. + * @return + */ + public boolean isDomainPropertiesAvailable() { + return false; + } + + /** + * Prepares the builder. Returns a list of the number of equivalence classes per level + * + * @param data In prioritized order, with highest priority to lowest priority + * @return + */ + public int[] prepare(String[] data){ + + // Check + prepareResult(data); + + // Compute + int[] sizes = new int[this.result[0].length]; + for (int i=0; i < sizes.length; i++){ + Set set = new HashSet(); + for (int j=0; j() { + @Override + public int compare(String o1, String o2) { + try { + return (priority == Priority.HIGHEST_TO_LOWEST) ? -type.compare(o1, o2) : type.compare(o1, o2); + } catch (NumberFormatException | ParseException e) { + throw new RuntimeException(e); + } + } + }); + // Based on provided priorities + } else { + + // Create ordered values for this case + List order = new ArrayList(); + + // Create sorted list of priorities + String[] priorities = data.clone(); + Arrays.sort(priorities, new Comparator() { + @Override + public int compare(String o1, String o2) { + int prio1 = HierarchyBuilderPriorityBased.this.priorities.getOrDefault(o1, 0); + int prio2 = HierarchyBuilderPriorityBased.this.priorities.getOrDefault(o2, 0); + return (priority == Priority.HIGHEST_TO_LOWEST) ? -Integer.compare(prio1, prio2) : Integer.compare(prio1, prio2); + } + }); + + // Prepare sets + Set setOfPriorities = new HashSet(); + setOfPriorities.addAll(Arrays.asList(priorities)); + Set setOfData = new HashSet(); + setOfData.addAll(Arrays.asList(data)); + + // Add elements + for (String element : priorities) { + if (setOfData.contains(element)) { + order.add(element); + } + } + + // Perform check + if (!setOfPriorities.containsAll(setOfData)) { + throw new IllegalArgumentException("Data contains elements that haven't been prioritized"); + } + + // Done + values = order.toArray(new String[order.size()]); + } + + // Prepare + int levels = Math.min(values.length, maxLevels); + double offset = (double)values.length / (double)levels; + + // Result + this.result = new String[values.length][]; + + // Prepare arrays for levels + for (int i=0; i < result.length; i++) { + this.result[i] = new String[levels + 1]; + } + + // For each level + for (int level = 0; level <= levels; level ++) { + for (int i = 0; i < values.length ; i++) { + // Value level + int valueLevel = levels - (int)Math.floor((double)(i) / offset); + // Value + this.result[i][level] = (valueLevel <= level ? DataType.ANY_VALUE : values[i]); + } + } + } +} \ No newline at end of file