Provide ResourceOrder implementation + minor additions.
This commit is contained in:
parent
8cc959a766
commit
ec04f02ed3
6 changed files with 307 additions and 38 deletions
|
@ -2,6 +2,8 @@ package jobshop;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class BestKnownResult {
|
public class BestKnownResult {
|
||||||
|
|
||||||
|
@ -9,6 +11,13 @@ public class BestKnownResult {
|
||||||
return bests.containsKey(instanceName);
|
return bests.containsKey(instanceName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static List<String> instancesMatching(String namePrefix) {
|
||||||
|
return Arrays.stream(instances)
|
||||||
|
.filter(i -> i.startsWith(namePrefix))
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
public static int of(String instanceName) {
|
public static int of(String instanceName) {
|
||||||
if(!bests.containsKey(instanceName)) {
|
if(!bests.containsKey(instanceName)) {
|
||||||
throw new RuntimeException("Unknown best result for "+instanceName);
|
throw new RuntimeException("Unknown best result for "+instanceName);
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package jobshop;
|
package jobshop;
|
||||||
|
|
||||||
|
import jobshop.encodings.Task;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
@ -24,9 +26,15 @@ public class Instance {
|
||||||
public int duration(int job, int task) {
|
public int duration(int job, int task) {
|
||||||
return durations[job][task];
|
return durations[job][task];
|
||||||
}
|
}
|
||||||
|
public int duration(Task t) {
|
||||||
|
return duration(t.job, t.task);
|
||||||
|
}
|
||||||
public int machine(int job, int task) {
|
public int machine(int job, int task) {
|
||||||
return machines[job][task];
|
return machines[job][task];
|
||||||
}
|
}
|
||||||
|
public int machine(Task t) {
|
||||||
|
return this.machine(t.job, t.task);
|
||||||
|
}
|
||||||
|
|
||||||
/** among the tasks of the given job, returns the task index that uses the given machine. */
|
/** among the tasks of the given job, returns the task index that uses the given machine. */
|
||||||
public int task_with_machine(int job, int wanted_machine) {
|
public int task_with_machine(int job, int wanted_machine) {
|
||||||
|
@ -46,6 +54,7 @@ public class Instance {
|
||||||
machines = new int[numJobs][numTasks];
|
machines = new int[numJobs][numTasks];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parses a instance from a file. */
|
||||||
public static Instance fromFile(Path path) throws IOException {
|
public static Instance fromFile(Path path) throws IOException {
|
||||||
Iterator<String> lines = Files.readAllLines(path).stream()
|
Iterator<String> lines = Files.readAllLines(path).stream()
|
||||||
.filter(l -> !l.startsWith("#"))
|
.filter(l -> !l.startsWith("#"))
|
||||||
|
|
|
@ -3,13 +3,13 @@ package jobshop;
|
||||||
import java.io.PrintStream;
|
import java.io.PrintStream;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
import jobshop.solvers.BasicSolver;
|
import jobshop.solvers.*;
|
||||||
import jobshop.solvers.RandomSolver;
|
|
||||||
import net.sourceforge.argparse4j.ArgumentParsers;
|
import net.sourceforge.argparse4j.ArgumentParsers;
|
||||||
import net.sourceforge.argparse4j.inf.ArgumentParser;
|
import net.sourceforge.argparse4j.inf.ArgumentParser;
|
||||||
import net.sourceforge.argparse4j.inf.ArgumentParserException;
|
import net.sourceforge.argparse4j.inf.ArgumentParserException;
|
||||||
|
@ -68,20 +68,23 @@ public class Main {
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
List<String> instances = ns.<String>getList("instance");
|
List<String> instancePrefixes = ns.getList("instance");
|
||||||
for(String instanceName : instances) {
|
List<String> instances = new ArrayList<>();
|
||||||
if(!BestKnownResult.isKnown(instanceName)) {
|
for(String instancePrefix : instancePrefixes) {
|
||||||
System.err.println("ERROR: instance \"" + instanceName + "\" is not avalaible.");
|
List<String> matches = BestKnownResult.instancesMatching(instancePrefix);
|
||||||
|
if(matches.isEmpty()) {
|
||||||
|
System.err.println("ERROR: instance prefix \"" + instancePrefix + "\" does not match any instance.");
|
||||||
System.err.println(" available instances: " + Arrays.toString(BestKnownResult.instances));
|
System.err.println(" available instances: " + Arrays.toString(BestKnownResult.instances));
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
}
|
}
|
||||||
|
instances.addAll(matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
float[] runtimes = new float[solversToTest.size()];
|
float[] runtimes = new float[solversToTest.size()];
|
||||||
float[] distances = new float[solversToTest.size()];
|
float[] distances = new float[solversToTest.size()];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
output.print( " ");;
|
output.print( " ");
|
||||||
for(String s : solversToTest)
|
for(String s : solversToTest)
|
||||||
output.printf("%-30s", s);
|
output.printf("%-30s", s);
|
||||||
output.println();
|
output.println();
|
||||||
|
@ -92,46 +95,46 @@ public class Main {
|
||||||
output.println();
|
output.println();
|
||||||
|
|
||||||
|
|
||||||
for(String instanceName : instances) {
|
for(String instanceName : instances) {
|
||||||
int bestKnown = BestKnownResult.of(instanceName);
|
int bestKnown = BestKnownResult.of(instanceName);
|
||||||
|
|
||||||
|
|
||||||
Path path = Paths.get("instances/", instanceName);
|
Path path = Paths.get("instances/", instanceName);
|
||||||
Instance instance = Instance.fromFile(path);
|
Instance instance = Instance.fromFile(path);
|
||||||
|
|
||||||
output.printf("%-8s %-5s %4d ",instanceName, instance.numJobs +"x"+instance.numTasks, bestKnown);
|
output.printf("%-8s %-5s %4d ",instanceName, instance.numJobs +"x"+instance.numTasks, bestKnown);
|
||||||
|
|
||||||
for(int solverId = 0 ; solverId < solversToTest.size() ; solverId++) {
|
for(int solverId = 0 ; solverId < solversToTest.size() ; solverId++) {
|
||||||
String solverName = solversToTest.get(solverId);
|
String solverName = solversToTest.get(solverId);
|
||||||
Solver solver = solvers.get(solverName);
|
Solver solver = solvers.get(solverName);
|
||||||
long start = System.currentTimeMillis();
|
long start = System.currentTimeMillis();
|
||||||
long deadline = System.currentTimeMillis() + solveTimeMs;
|
long deadline = System.currentTimeMillis() + solveTimeMs;
|
||||||
Result result = solver.solve(instance, deadline);
|
Result result = solver.solve(instance, deadline);
|
||||||
long runtime = System.currentTimeMillis() - start;
|
long runtime = System.currentTimeMillis() - start;
|
||||||
|
|
||||||
if(!result.schedule.isValid()) {
|
if(!result.schedule.isValid()) {
|
||||||
System.err.println("ERROR: solver returned an invalid schedule");
|
System.err.println("ERROR: solver returned an invalid schedule");
|
||||||
System.exit(1);
|
System.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert result.schedule.isValid();
|
||||||
|
int makespan = result.schedule.makespan();
|
||||||
|
float dist = 100f * (makespan - bestKnown) / (float) bestKnown;
|
||||||
|
runtimes[solverId] += (float) runtime / (float) instances.size();
|
||||||
|
distances[solverId] += dist / (float) instances.size();
|
||||||
|
|
||||||
|
output.printf("%7d %8s %5.1f ", runtime, makespan, dist);
|
||||||
|
output.flush();
|
||||||
}
|
}
|
||||||
|
output.println();
|
||||||
|
|
||||||
assert result.schedule.isValid();
|
|
||||||
int makespan = result.schedule.makespan();
|
|
||||||
float dist = 100f * (makespan - bestKnown) / (float) bestKnown;
|
|
||||||
runtimes[solverId] += (float) runtime / (float) instances.size();
|
|
||||||
distances[solverId] += dist / (float) instances.size();
|
|
||||||
|
|
||||||
output.printf("%7d %8s %5.1f ", runtime, makespan, dist);
|
|
||||||
output.flush();
|
|
||||||
}
|
}
|
||||||
output.println();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
output.printf("%-8s %-5s %4s ", "AVG", "-", "-");
|
output.printf("%-8s %-5s %4s ", "AVG", "-", "-");
|
||||||
for(int solverId = 0 ; solverId < solversToTest.size() ; solverId++) {
|
for(int solverId = 0 ; solverId < solversToTest.size() ; solverId++) {
|
||||||
output.printf("%7.1f %8s %5.1f ", runtimes[solverId], "-", distances[solverId]);
|
output.printf("%7.1f %8s %5.1f ", runtimes[solverId], "-", distances[solverId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,8 @@ import jobshop.Instance;
|
||||||
import jobshop.Schedule;
|
import jobshop.Schedule;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
/** Représentation par numéro de job. */
|
/** Représentation par numéro de job. */
|
||||||
public class JobNumbers extends Encoding {
|
public class JobNumbers extends Encoding {
|
||||||
|
@ -12,7 +14,7 @@ public class JobNumbers extends Encoding {
|
||||||
/** A numJobs * numTasks array containing the representation by job numbers. */
|
/** A numJobs * numTasks array containing the representation by job numbers. */
|
||||||
public final int[] jobs;
|
public final int[] jobs;
|
||||||
|
|
||||||
/** In case the encoding is only partially filled, indicates the index of first
|
/** In case the encoding is only partially filled, indicates the index of the first
|
||||||
* element of `jobs` that has not been set yet. */
|
* element of `jobs` that has not been set yet. */
|
||||||
public int nextToSet = 0;
|
public int nextToSet = 0;
|
||||||
|
|
||||||
|
@ -23,6 +25,31 @@ public class JobNumbers extends Encoding {
|
||||||
Arrays.fill(jobs, -1);
|
Arrays.fill(jobs, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JobNumbers(Schedule schedule) {
|
||||||
|
super(schedule.pb);
|
||||||
|
|
||||||
|
this.jobs = new int[instance.numJobs * instance.numTasks];
|
||||||
|
|
||||||
|
// for each job indicates which is the next task to be scheduled
|
||||||
|
int[] nextOnJob = new int[instance.numJobs];
|
||||||
|
|
||||||
|
while(Arrays.stream(nextOnJob).anyMatch(t -> t < instance.numTasks)) {
|
||||||
|
Task next = IntStream
|
||||||
|
// for all jobs numbers
|
||||||
|
.range(0, instance.numJobs)
|
||||||
|
// build the next task for this job
|
||||||
|
.mapToObj(j -> new Task(j, nextOnJob[j]))
|
||||||
|
// only keep valid tasks (some jobs have no task left to be executed)
|
||||||
|
.filter(t -> t.task < instance.numTasks)
|
||||||
|
// select the task with the earliest execution time
|
||||||
|
.min(Comparator.comparing(t -> schedule.startTime(t.job, t.task)))
|
||||||
|
.get();
|
||||||
|
|
||||||
|
this.jobs[nextToSet++] = next.job;
|
||||||
|
nextOnJob[next.job] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Schedule toSchedule() {
|
public Schedule toSchedule() {
|
||||||
// time at which each machine is going to be freed
|
// time at which each machine is going to be freed
|
||||||
|
|
131
src/main/java/jobshop/encodings/ResourceOrder.java
Normal file
131
src/main/java/jobshop/encodings/ResourceOrder.java
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package jobshop.encodings;
|
||||||
|
|
||||||
|
import jobshop.Encoding;
|
||||||
|
import jobshop.Instance;
|
||||||
|
import jobshop.Schedule;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
public class ResourceOrder extends Encoding {
|
||||||
|
|
||||||
|
// for each machine m, taskByMachine[m] is an array of tasks to be
|
||||||
|
// executed on this machine in the same order
|
||||||
|
public final Task[][] tasksByMachine;
|
||||||
|
|
||||||
|
// for each machine, indicate on many tasks have been initialized
|
||||||
|
public final int[] nextFreeSlot;
|
||||||
|
|
||||||
|
/** Creates a new empty resource order. */
|
||||||
|
public ResourceOrder(Instance instance)
|
||||||
|
{
|
||||||
|
super(instance);
|
||||||
|
|
||||||
|
// matrix of null elements (null is the default value of objects)
|
||||||
|
tasksByMachine = new Task[instance.numMachines][instance.numJobs];
|
||||||
|
|
||||||
|
// no task scheduled on any machine (0 is the default value)
|
||||||
|
nextFreeSlot = new int[instance.numMachines];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates a resource order from a schedule. */
|
||||||
|
public ResourceOrder(Schedule schedule)
|
||||||
|
{
|
||||||
|
super(schedule.pb);
|
||||||
|
Instance pb = schedule.pb;
|
||||||
|
|
||||||
|
this.tasksByMachine = new Task[pb.numMachines][];
|
||||||
|
this.nextFreeSlot = new int[instance.numMachines];
|
||||||
|
|
||||||
|
for(int m = 0 ; m<schedule.pb.numMachines ; m++) {
|
||||||
|
final int machine = m;
|
||||||
|
|
||||||
|
// for thi machine, find all tasks that are executed on it and sort them by their start time
|
||||||
|
tasksByMachine[m] =
|
||||||
|
IntStream.range(0, pb.numJobs) // all job numbers
|
||||||
|
.mapToObj(j -> new Task(j, pb.task_with_machine(j, machine))) // all tasks on this machine (one per job)
|
||||||
|
.sorted(Comparator.comparing(t -> schedule.startTime(t.job, t.task))) // sorted by start time
|
||||||
|
.toArray(Task[]::new); // as new array and store in tasksByMachine
|
||||||
|
|
||||||
|
// indicate that all tasks have been initialized for machine m
|
||||||
|
nextFreeSlot[m] = instance.numJobs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Schedule toSchedule() {
|
||||||
|
// indicate for each task that have been scheduled, its start time
|
||||||
|
int [][] startTimes = new int [instance.numJobs][instance.numTasks];
|
||||||
|
|
||||||
|
// for each job, how many tasks have been scheduled (0 initially)
|
||||||
|
int[] nextToScheduleByJob = new int[instance.numJobs];
|
||||||
|
|
||||||
|
// for each machine, how many tasks have been scheduled (0 initially)
|
||||||
|
int[] nextToScheduleByMachine = new int[instance.numMachines];
|
||||||
|
|
||||||
|
// for each machine, earliest time at which the machine can be used
|
||||||
|
int[] releaseTimeOfMachine = new int[instance.numMachines];
|
||||||
|
|
||||||
|
|
||||||
|
// loop while there remains a job that has unscheduled tasks
|
||||||
|
while(IntStream.range(0, instance.numJobs).anyMatch(m -> nextToScheduleByJob[m] < instance.numTasks)) {
|
||||||
|
|
||||||
|
// selects a task that has noun scheduled predecessor on its job and machine :
|
||||||
|
// - it is the next to be schedule on a machine
|
||||||
|
// - it is the next to be scheduled on its job
|
||||||
|
// if there is no such task, we have cyclic dependency and the solution is invalid
|
||||||
|
Optional<Task> schedulable =
|
||||||
|
IntStream.range(0, instance.numMachines) // all machines ...
|
||||||
|
.filter(m -> nextToScheduleByMachine[m] < instance.numJobs) // ... with unscheduled jobs
|
||||||
|
.mapToObj(m -> this.tasksByMachine[m][nextToScheduleByMachine[m]]) // tasks that are next to schedule on a machine ...
|
||||||
|
.filter(task -> task.task == nextToScheduleByJob[task.job]) // ... and on their job
|
||||||
|
.findFirst(); // select the first one if any
|
||||||
|
|
||||||
|
if(schedulable.isPresent()) {
|
||||||
|
// we found a schedulable task, lets call it t
|
||||||
|
Task t = schedulable.get();
|
||||||
|
int machine = instance.machine(t.job, t.task);
|
||||||
|
|
||||||
|
// compute the earliest start time (est) of the task
|
||||||
|
int est = t.task == 0 ? 0 : startTimes[t.job][t.task-1] + instance.duration(t.job, t.task-1);
|
||||||
|
est = Math.max(est, releaseTimeOfMachine[instance.machine(t)]);
|
||||||
|
startTimes[t.job][t.task] = est;
|
||||||
|
|
||||||
|
// mark the task as scheduled
|
||||||
|
nextToScheduleByJob[t.job]++;
|
||||||
|
nextToScheduleByMachine[machine]++;
|
||||||
|
// increase the release time of the machine
|
||||||
|
releaseTimeOfMachine[machine] = est + instance.duration(t.job, t.task);
|
||||||
|
} else {
|
||||||
|
// no tasks are schedulable, there is no solution for this resource ordering
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we exited the loop : all tasks have been scheduled successfully
|
||||||
|
return new Schedule(instance, startTimes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates an exact copy of this resource order. */
|
||||||
|
public ResourceOrder copy() {
|
||||||
|
return new ResourceOrder(this.toSchedule());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
StringBuilder s = new StringBuilder();
|
||||||
|
for(int m=0; m < instance.numMachines; m++)
|
||||||
|
{
|
||||||
|
s.append("Machine ").append(m).append(" : ");
|
||||||
|
for(int j=0; j<instance.numJobs; j++)
|
||||||
|
{
|
||||||
|
s.append(tasksByMachine[m][j]).append(" ; ");
|
||||||
|
}
|
||||||
|
s.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
90
src/main/java/jobshop/solvers/DescentSolver.java
Normal file
90
src/main/java/jobshop/solvers/DescentSolver.java
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package jobshop.solvers;
|
||||||
|
|
||||||
|
import jobshop.Instance;
|
||||||
|
import jobshop.Result;
|
||||||
|
import jobshop.Solver;
|
||||||
|
import jobshop.encodings.ResourceOrder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class DescentSolver implements Solver {
|
||||||
|
|
||||||
|
/** A block represents a subsequence of the critical path such that all tasks in it execute on the same machine.
|
||||||
|
* This class identifies a block in a ResourceOrder representation.
|
||||||
|
*
|
||||||
|
* Consider the solution in ResourceOrder representation
|
||||||
|
* machine 0 : (0,1) (1,2) (2,2)
|
||||||
|
* machine 1 : (0,2) (2,1) (1,1)
|
||||||
|
* machine 2 : ...
|
||||||
|
*
|
||||||
|
* The block with : machine = 1, firstTask= 0 and lastTask = 1
|
||||||
|
* Represent the task sequence : [(0,2) (2,1)]
|
||||||
|
*
|
||||||
|
* */
|
||||||
|
static class Block {
|
||||||
|
/** machine on which the block is identified */
|
||||||
|
final int machine;
|
||||||
|
/** index of the first task of the block */
|
||||||
|
final int firstTask;
|
||||||
|
/** index of the last task of the block */
|
||||||
|
final int lastTask;
|
||||||
|
|
||||||
|
Block(int machine, int firstTask, int lastTask) {
|
||||||
|
this.machine = machine;
|
||||||
|
this.firstTask = firstTask;
|
||||||
|
this.lastTask = lastTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a swap of two tasks on the same machine in a ResourceOrder encoding.
|
||||||
|
*
|
||||||
|
* Consider the solution in ResourceOrder representation
|
||||||
|
* machine 0 : (0,1) (1,2) (2,2)
|
||||||
|
* machine 1 : (0,2) (2,1) (1,1)
|
||||||
|
* machine 2 : ...
|
||||||
|
*
|
||||||
|
* The swam with : machine = 1, t1= 0 and t2 = 1
|
||||||
|
* Represent inversion of the two tasks : (0,2) and (2,1)
|
||||||
|
* Applying this swap on the above resource order should result in the following one :
|
||||||
|
* machine 0 : (0,1) (1,2) (2,2)
|
||||||
|
* machine 1 : (2,1) (0,2) (1,1)
|
||||||
|
* machine 2 : ...
|
||||||
|
*/
|
||||||
|
static class Swap {
|
||||||
|
// machine on which to perform the swap
|
||||||
|
final int machine;
|
||||||
|
// index of one task to be swapped
|
||||||
|
final int t1;
|
||||||
|
// index of the other task to be swapped
|
||||||
|
final int t2;
|
||||||
|
|
||||||
|
Swap(int machine, int t1, int t2) {
|
||||||
|
this.machine = machine;
|
||||||
|
this.t1 = t1;
|
||||||
|
this.t2 = t2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Apply this swap on the given resource order, transforming it into a new solution. */
|
||||||
|
public void applyOn(ResourceOrder order) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Result solve(Instance instance, long deadline) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns a list of all blocks of the critical path. */
|
||||||
|
List<Block> blocksOfCriticalPath(ResourceOrder order) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** For a given block, return the possible swaps for the Nowicki and Smutnicki neighborhood */
|
||||||
|
List<Swap> neighbors(Block block) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue