Flag Quiz application (throwIndexOutOfBoundsException) - java

Essentially what I made is a flag quiz app that tests how many flags you guess correctly and gives you a percentage based score at the end.
The problem is that when I do the quiz I sometimes get an outOfBounds error and the app crashes instead of showing the score. It is pretty random because other times I can do the app 3-4 times no problems with the score displaying correctly at the end.
These are the 2 classes used when resetting the quiz at the end, The problem is String nextImage = quizCountriesList.remove(0); inside of loadNextFlag()
the exact error is:
at
java.util.ArrayList.throwIndexOutOfBoundsException(ArrayList.java:255)
at java.util.ArrayList.remove(ArrayList.java:403)
at
com.map524s1a.flagquizdk.MainActivityFragment.loadNextFlag(MainActivityFragment.java:180)
public void resetQuiz() {
// use AssetManager to get image file names for enabled regions
AssetManager assets = getActivity().getAssets();
fileNameList.clear(); // empty list of image file names
try {
// loop through each region
for (String region : regionsSet) {
// get a list of all flag image files in this region
String[] paths = assets.list(region);
for (String path : paths)
fileNameList.add(path.replace(".png", ""));
}
} catch (IOException ex) {
Log.e(TAG, "Error loading image file names", ex);
}
correctAnswers = 0; // reset the number of correct answers made
questions = 0; // reset the total number of questions
quizCountriesList.clear(); // clear prior list of quiz countries
int flagCounter = 1;
int numberOfFlags = fileNameList.size();
// add FLAGS_IN_QUIZ random file names to the quizCountriesList
while (flagCounter <= FLAGS_IN_QUIZ) {
int randomIndex = random.nextInt(numberOfFlags);
// get the random file name
String filename = fileNameList.get(randomIndex);
// if the region is enabled and it hasn't already been chosen
if (!quizCountriesList.contains(filename)) {
quizCountriesList.add(filename); // add the file to the list
++flagCounter;
}
}
loadNextFlag(); // start the quiz by loading the first flag
}
// after the user guesses a correct flag, load the next flag
private void loadNextFlag() {
// get file name of the next flag and remove it from the list
String nextImage = quizCountriesList.remove(0);
correctAnswer = nextImage; // update the correct answer
answerTextView.setText(""); // clear answerTextView
// display current question number
questionNumberTextView.setText(getString(R.string.question, (questions + 1), FLAGS_IN_QUIZ));
// extract the region from the next image's name
String region = nextImage.substring(0, nextImage.indexOf('-'));
// use AssetManager to load next image from assets folder
AssetManager assets = getActivity().getAssets();
// get an InputStream to the asset representing the next flag
// and try to use the InputStream
try (InputStream stream =
assets.open(region + "/" + nextImage + ".png")) {
// load the asset as a Drawable and display on the flagImageView
Drawable flag = Drawable.createFromStream(stream, nextImage);
flagImageView.setImageDrawable(flag);
animate(false); // animate the flag onto the screen
}
catch (IOException exception) {
Log.e(TAG, "Error loading " + nextImage, exception);
}
Collections.shuffle(fileNameList); // shuffle file names
// put the correct answer at the end of fileNameList
int correct = fileNameList.indexOf(correctAnswer);
fileNameList.add(fileNameList.remove(correct));
// add 2, 4, 6 or 8 guess Buttons based on the value of guessRows
for (int row = 0; row < guessRows; row++) {
// place Buttons in currentTableRow
for (int column = 0;
column < guessLinearLayouts[row].getChildCount();
column++) {
// get reference to Button to configure
Button newGuessButton =
(Button) guessLinearLayouts[row].getChildAt(column);
newGuessButton.setEnabled(true);
// get country name and set it as newGuessButton's text
String filename = fileNameList.get((row * 2) + column);
newGuessButton.setText(getCountryName(filename));
}
}
// randomly replace one Button with the correct answer
int row = random.nextInt(guessRows); // pick random row
int column = random.nextInt(2); // pick random column
LinearLayout randomRow = guessLinearLayouts[row]; // get the row
String countryName = getCountryName(correctAnswer);
((Button) randomRow.getChildAt(column)).setText(countryName);
}

Related

Using JT400, how do I download a PrintObjectTransformedInputStream pdf file to my desktop?

I am writing a program in Java using JT400. The program downloads certain spool files from AS400 to the client machine.
The process is roughly thus:
Connect
Access specific user's spool files
Loop through files until specific spool file found
Download that spool file to a local folder
Disconnect
My current problem is that I don't know how to download the file to local directory.
I have a PrintObjectTransformedInputStream pdf file, but I don't know how to work with that.
Code below:
static void hrzn(List<String> strUser, // Users to search for
List<String> lisReports, // List of reports to search and download for the user
int ipYear, // Year parameter
int ipMonth, // Month parameter
int ipDay, // Day parameter
String strPath // Path where spool file will be downloaded to
) throws AS400SecurityException, RequestNotSupportedException, IOException, InterruptedException, ErrorCompletingRequestException, ObjectDoesNotExistException, OpenListException {
// Sign in to AS400
String strUserLogin = "*******************************";
String strPass = "***************************";
AS400 hrznAS400 = new AS400("*************", strUserLogin, strPass);
//User hrznUser = new User(hrznAS400, strUser);
System.out.println(strUser.size());
// Iterate through each user in the user list
for (int i = 0; i < strUser.size(); i++) {
System.out.println("User: " + strUser.get(i));
// Create SpooledFileOpenList of the output queue of the user
SpooledFileOpenList list = new SpooledFileOpenList(hrznAS400);
// Filter spool file list to specific user
list.setFilterUsers(new String[]{strUser.get(i)});
// Sort by date create. False means descending order;
list.addSortField(SpooledFileOpenList.DATE_OPENED, false);
// Get the filtered list of spool files
list.open();
Enumeration enumFiles = list.getItems();
// Loop through each of the spool files in the users output queue
while (enumFiles.hasMoreElements()) {
SpooledFileListItem item = (SpooledFileListItem) enumFiles.nextElement();
System.out.println("User ID: " + String.valueOf(strUser) + " File: " + item.getUserData() + " File Creation Date: " + item.getCreationDate());
// If the current spool file type is desired, execute code
if (lisReports.contains(item.getUserData().trim())) {
System.out.println("File: " + item.getUserData() + " Date Created: " + item.getCreationDate());
// Create a spooled file
SpooledFile splF = new SpooledFile(
hrznAS400, // AS400
item.getName(), // SPLF NAME
item.getNumber(), // SPFL NUMBER
item.getJobName(), // JOB NAME
item.getJobUser(), // JOB USER
item.getJobNumber() // JOB NUMBER
); // https://javadoc.io/static/net.sf.jt400/jt400/11.0/com/ibm/as400/access/SpooledFile.html
// Get printer device type
String strPrType = splF.getStringAttribute(PrintObject.ATTR_PRTDEVTYPE);
// Extract date/time info from item fields
LocalDateTime localDate = LocalDateTime.from(item.getCreationDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
int year = localDate.getYear();
int month = localDate.getMonthValue();
int day = localDate.getDayOfMonth();
int hour = localDate.getHour();
int minute = localDate.getMinute();
int second = localDate.getSecond();
// If the printer device type equals *SCS, proceed with PDF transform
if (strPrType.equals("*SCS")) {
System.out.println("Created instance of spool file");
// Set up print parameter list
PrintParameterList printParms = new PrintParameterList();
printParms.setParameter(PrintObject.ATTR_WORKSTATION_CUST_OBJECT, "/QSYS.LIB/QCTXPDF.WSCST"); // https://javadoc.io/static/net.sf.jt400/jt400/11.0/com/ibm/as400/access/doc-files/PrintAttributes.html#HDRKEYIFS_8
printParms.setParameter(PrintObject.ATTR_MFGTYPE, "*WSCST");
printParms.setParameter(PrintObject.ATTR_DEVTYPE, "*AFPDS");
// Create a transformed input stream from the spooled file
PrintObjectTransformedInputStream is = splF.getTransformedInputStream(printParms); //https://www.ibm.com/docs/en/i/7.4?topic=considerations-workstation-customizing-object-wscst-parameter
System.out.println("Transformed spool file to PDF");
}
}
}
list.close();
}
}

Converting line and column coordinate to a caret position for a JSON debugger

I am building a small Java utility (using Jackson) to catch errors in Java files, and one part of it is a text area, in which you might paste some JSON context and it will tell you the line and column where it's found it:
I am using the error message to take out the line and column as a string and print it out in the interface for someone using it.
This is the JSON sample I'm working with, and there is an intentional error beside "age", where it's missing a colon:
{
"name": "mkyong.com",
"messages": ["msg 1", "msg 2", "msg 3"],
"age" 100
}
What I want to do is also highlight the problematic area in a cyan color, and for that purpose, I have this code for the button that validates what's inserted in the text area:
cmdValidate.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
functionsClass ops = new functionsClass();
String JSONcontent = JSONtextArea.getText();
Results obj = new Results();
ops.validate_JSON_text(JSONcontent, obj);
String result = obj.getResult();
String caret = obj.getCaret();
//String lineNum = obj.getLineNum();
//showStatus(result);
if(result==null) {
textAreaError.setText("JSON code is valid!");
} else {
textAreaError.setText(result);
Highlighter.HighlightPainter cyanPainter;
cyanPainter = new DefaultHighlighter.DefaultHighlightPainter(Color.cyan);
int caretPosition = Integer.parseInt(caret);
int lineNumber = 0;
try {
lineNumber = JSONtextArea.getLineOfOffset(caretPosition);
} catch (BadLocationException e2) {
// TODO Auto-generated catch block
e2.printStackTrace();
}
try {
JSONtextArea.getHighlighter().addHighlight(lineNumber, caretPosition + 1, cyanPainter);
} catch (BadLocationException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
}
}
});
}
The "addHighlight" method works with a start range, end range and a color, which didn't become apparent to me immediately, thinking I had to get the reference line based on the column number. Some split functions to extract the numbers, I assigned 11 (in screenshot) to a caret value, not realizing that it only counts character positions from the beginning of the string and represents the end point of the range.
For reference, this is the class that does the work behind the scenes, and the error handling at the bottom is about extracting the line and column numbers. For the record, "x" is the error message that would generate out of an invalid file.
package parsingJSON;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
public class functionsClass extends JSONTextCompare {
public boolean validate_JSON_text(String JSONcontent, Results obj) {
boolean valid = false;
try {
ObjectMapper objMapper = new ObjectMapper();
JsonNode validation = objMapper.readTree(JSONcontent);
valid = true;
}
catch (JsonParseException jpe){
String x = jpe.getMessage();
printTextArea(x, obj);
//return part_3;
}
catch (IOException ioe) {
String x = ioe.getMessage();
printTextArea(x, obj);
//return part_3;
}
return valid;
}
public void printTextArea(String x, Results obj) {
// TODO Auto-generated method stub
System.out.println(x);
String err = x.substring(x.lastIndexOf("\n"));
String parts[] = err.split(";");
//String part 1 is the discarded leading edge that is the closing brackets of the JSON content
String part_2 = parts[1];
//split again to get rid of the closing square bracket
String parts2[] = part_2.split("]");
String part_3 = parts2[0];
//JSONTextCompare feedback = new JSONTextCompare();
//split the output to get the exact location of the error to communicate back and highlight it in the JSONTextCompare class
//first need to get the line number from the output
String[] parts_lineNum = part_3.split("line: ");
String[] parts_lineNum_final = parts_lineNum[1].split(", column:");
String lineNum = parts_lineNum_final[0];
String[] parts_caret = part_3.split("column: ");
String caret = parts_caret[1];
System.out.println(caret);
obj.setLineNum(lineNum);
obj.setCaret(caret);
obj.setResult(part_3);
System.out.println(part_3);
}
}
Screenshot for what the interface currently looks like:
Long story short - how do I turn the coordinates Line 4, Col 11 into a caret value (e.g. it's value 189, for the sake of argument) that I can use to get the highlighter to work properly. Some kind of custom parsing formula might be possible, but in general, is that even possible to do?
how do I turn the coordinates Line 4, Col 11 into a caret value (e.g. it's value 189,
Check out: Text Utilities for methods that might be helpful when working with text components. It has methods like:
centerLineInScrollPane
getColumnAtCaret
getLineAtCaret
getLines
gotoStartOfLine
gotoFirstWordOnLine
getWrappedLines
In particular the gotoStartOfLine() method contains code you can modify to get the offset of the specified row/column.offset.
The basic code would be:
int line = 4;
int column = 11;
Element root = textArea.getDocument().getDefaultRootElement();
int offset = root.getElement( line - 1 ).getStartOffset() + column;
System.out.println(offset);
The way it works is essentially counting the number of characters in each line, up until the line in which the error is occurring, and adding the caretPosition to that sum of characters, which is what the Highlighter needs to apply the marking to the correct location.
I've added the code for the Validate button for context.
functionsClass ops = new functionsClass();
String JSONcontent = JSONtextArea.getText();
Results obj = new Results();
ops.validate_JSON_text(JSONcontent, obj);
String result = obj.getResult();
String caret = obj.getCaret();
String lineNum = obj.getLineNum();
//showStatus(result);
if(result==null) {
textAreaError.setText("JSON code is valid!");
} else {
textAreaError.setText(result);
Highlighter.HighlightPainter cyanPainter;
cyanPainter = new DefaultHighlighter.DefaultHighlightPainter(Color.cyan);
//the column number as per the location of the error
int caretPosition = Integer.parseInt(caret); //JSONtextArea.getCaretPosition();
//the line number as per the location of the error
int lineNumber = Integer.parseInt(lineNum);
//get the number of characters in the string up to the line in which the error is found
int totalChars = 0;
int counter = 0; //used to only go to the line above where the error is located
String[] lines = JSONcontent.split("\\r?\\n");
for (String line : lines) {
counter = counter + 1;
//as long as we're above the line of the error (lineNumber variable), keep counting characters
if (counter < lineNumber)
{
totalChars = totalChars + line.length();
}
//if we are at the line that contains the error, only add the caretPosition value to get the final position where the highlighting should go
if (counter == lineNumber)
{
totalChars = totalChars + caretPosition;
break;
}
}
//put down the highlighting in the area where the JSON file is having a problem
try {
JSONtextArea.getHighlighter().addHighlight(totalChars - 2, totalChars + 2, cyanPainter);
} catch (BadLocationException e1) {
// TODO Auto-generated catch block
e1.getMessage();
}
}
The contents of the JSON file is treated as a string, and that's why I'm also iterating through it in that fashion. There are certainly better ways to go through lines in the string, and I'll add some reference topics on SO:
What is the easiest/best/most correct way to iterate through the characters of a string in Java? - Link
Check if a string contains \n - Link
Split Java String by New Line - Link
What is the best way to iterate over the lines of a Java String? - Link
Generally a combination of these led to this solution, and I am also not targeting it for use on very large JSON files.
A screenshot of the output, with the interface highlighting the same area that Notepad++ would complain about, if it could debug code:
I'll post the project on GitHub after I clean it up and comment it some, and will give a link to that later, but for now, hopefully this helps the next dev in a similar situation.

Reading a text file with boolean values into an array list as objects

I am trying to read a file of string int and boolean values into an array list as object blocks. The string values go into the array list just fine, its the boolean values I'm having trouble with. Every time I encounter the variable 'active'there is a mismatch exception. Please help! The text file for if the block is a wizard goes in this order
name (string)
location (string)
active (boolean) ... the one I'm having issues with
skill level (int)
friendliness (int)
I included the driver class as well as the Witch class which contains the
variable 'active' originally.
Driver class that adds objects to the array list based on what the scanner
reads from the file
package project2;
import java.util.Scanner;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
public class Project2 {
public static void main(String[] args) {
Scanner inputFileScanner1 = null;
//file name
String listFile = "list.txt";
// Check to see if file exists
try {
inputFileScanner1 = new Scanner(new File(listFile));
} catch (FileNotFoundException e) {
System.out.println("Error opening file.");
System.exit(1);
}
//create Individuals arraylist and Location arraylist
ArrayList < Individual > Individual = new ArrayList < > ();
ArrayList < String > Location = new ArrayList < > ();
//declare variables to read file contents into the arraylist
String wizName, witchName, individualName, location, position,
profession = null, line = null;
int wizLevel, witchSkillLevel, friendliness;
boolean active;
//while there is a next line, if the line equals Wizard, the next four lines
// are wizard name, location, position and level
while (inputFileScanner1.hasNext()) {
line = inputFileScanner1.nextLine();
if (line.trim().equals("Wizard")) {
wizName = inputFileScanner1.nextLine().trim();
location = inputFileScanner1.nextLine().trim();
position = inputFileScanner1.nextLine().trim();
wizLevel = inputFileScanner1.nextInt();
//create wizard object
Individual wizard = new Wizard(wizName, location, position, profession, wizLevel);
//fill arraylist with wizard objects
Individual.add(wizard);
Location.add(location);
} //if the next line is Witch, the next five lines are
// witch name, location, yes/no active, skill level, and friendliness
//in that order
else if (line.trim().equals("Witch")) {
witchName = inputFileScanner1.nextLine().trim();
location = inputFileScanner1.nextLine().trim();
active = inputFileScanner1.nextBoolean();
witchSkillLevel = inputFileScanner1.nextInt();
friendliness = inputFileScanner1.nextInt();
//create witch object
Individual witch = new Witch(witchName, location, profession, witchSkillLevel, friendliness, active);
//fill the arraylist with witch objects
Individual.add(witch);
Location.add(location);
} else {
profession = line.trim();
individualName = inputFileScanner1.nextLine().trim();
location = inputFileScanner1.nextLine().trim();
Individual i = new Individual(profession, individualName, location);
Individual.add(i);
Location.add(location);
}
java.util.Collections.sort(Individual);
java.util.Collections.sort(Location);
}
System.out.println("List of friends and possible allies: " + Location);
inputFileScanner1.close();
}
}
//Witch class which holds values that are in the text file. active is the boolean value Im having trouble with
package project2;
public class Witch extends Individual implements Magical {
private int skill;
private int friendly;
//Constructor with witch parameters
public Witch(String name, String location, String profession,
int skill, int friendly, boolean active) {
}
//default constructor
public Witch() {
this("", "", "", 0, 0, false);
}
//overridden abstract method from magical interface
#Override
public void assess() {
System.out.print(this.friendly + " " + this.skill + " " + super.toString());
}
}
<!-- end snippet -->
Text file :
enter image description here
When you pull in your boolean variable do something like this.
if(inputFileScanner1.nextLine().trim().equals("yes"))
{
active = true;
}
else
{
active = false;
}
Okay, the problem is that the file contains the strings yes and no, that are not directly parsable as booleans (should be true or false).
If you can change the original data file somehow, I would suggest to use the two true and false keywords, otherwise, the #Sendrick Jefferson solution will do the job (at your own risk: every typo, as for instance "ye", will be translated into false).

LensKit: LensKit demo is not reading my data file

When I run the LensKit demo program I get this error:
[main] ERROR org.grouplens.lenskit.data.dao.DelimitedTextRatingCursor - C:\Users\sean\Desktop\ml-100k\u - Copy.data:4: invalid input, skipping line
I reworked the ML 100k data set so that it only holds this line although I dont see how this would effect it:
196 242 3 881250949
186 302 3 891717742
22 377 1 878887116
244
Here is the code I am using too:
public class HelloLenskit implements Runnable {
public static void main(String[] args) {
HelloLenskit hello = new HelloLenskit(args);
try {
hello.run();
} catch (RuntimeException e) {
System.err.println(e.getMessage());
System.exit(1);
}
}
private String delimiter = "\t";
private File inputFile = new File("C:\\Users\\sean\\Desktop\\ml-100k\\u - Copy.data");
private List<Long> users;
public HelloLenskit(String[] args) {
int nextArg = 0;
boolean done = false;
while (!done && nextArg < args.length) {
String arg = args[nextArg];
if (arg.equals("-e")) {
delimiter = args[nextArg + 1];
nextArg += 2;
} else if (arg.startsWith("-")) {
throw new RuntimeException("unknown option: " + arg);
} else {
inputFile = new File(arg);
nextArg += 1;
done = true;
}
}
users = new ArrayList<Long>(args.length - nextArg);
for (; nextArg < args.length; nextArg++) {
users.add(Long.parseLong(args[nextArg]));
}
}
public void run() {
// We first need to configure the data access.
// We will use a simple delimited file; you can use something else like
// a database (see JDBCRatingDAO).
EventDAO base = new SimpleFileRatingDAO(inputFile, "\t");
// Reading directly from CSV files is slow, so we'll cache it in memory.
// You can use SoftFactory here to allow ratings to be expunged and re-read
// as memory limits demand. If you're using a database, just use it directly.
EventDAO dao = new EventCollectionDAO(Cursors.makeList(base.streamEvents()));
// Second step is to create the LensKit configuration...
LenskitConfiguration config = new LenskitConfiguration();
// ... configure the data source
config.bind(EventDAO.class).to(dao);
// ... and configure the item scorer. The bind and set methods
// are what you use to do that. Here, we want an item-item scorer.
config.bind(ItemScorer.class)
.to(ItemItemScorer.class);
// let's use personalized mean rating as the baseline/fallback predictor.
// 2-step process:
// First, use the user mean rating as the baseline scorer
config.bind(BaselineScorer.class, ItemScorer.class)
.to(UserMeanItemScorer.class);
// Second, use the item mean rating as the base for user means
config.bind(UserMeanBaseline.class, ItemScorer.class)
.to(ItemMeanRatingItemScorer.class);
// and normalize ratings by baseline prior to computing similarities
config.bind(UserVectorNormalizer.class)
.to(BaselineSubtractingUserVectorNormalizer.class);
// There are more parameters, roles, and components that can be set. See the
// JavaDoc for each recommender algorithm for more information.
// Now that we have a factory, build a recommender from the configuration
// and data source. This will compute the similarity matrix and return a recommender
// that uses it.
Recommender rec = null;
try {
rec = LenskitRecommender.build(config);
} catch (RecommenderBuildException e) {
throw new RuntimeException("recommender build failed", e);
}
// we want to recommend items
ItemRecommender irec = rec.getItemRecommender();
assert irec != null; // not null because we configured one
// for users
for (long user: users) {
// get 10 recommendation for the user
List<ScoredId> recs = irec.recommend(user, 10);
System.out.format("Recommendations for %d:\n", user);
for (ScoredId item: recs) {
System.out.format("\t%d\n", item.getId());
}
}
}
}
I am really lost on this one and would appreciate any help. Thanks for your time.
The last line of your input file only contains one field. Each input file line needs to contain 3 or 4 fields.

How to search a directory of mp3 files and filter results by artists ?

I'm trying to find out how to list the music on my computer by a certain artist. I understand how to list all the music in the directory but I can't find a way to show only the music from one of the artists. Below is the code I'm using to list all the music in the directory.
How do I search the meta data for the artist's name and filter those files? Feel free to comment on the code if that's not a good way to list the files, I'm pretty new to all of this.
Code Fragment:
File musicList = new File("C:\\Users\\Music");
String[] fileNames = musicList.list();
String songsResponse = "";
int number = 1;
for(int i = 0; i < fileNames.length; i++)
{
if(fileNames[i].endsWith(".mp3"))
{
songsResponse += (number + ". ") + fileNames[i] + "\n";
number++;
}
}
You could also try Java ID3 Tag Library, which supports ID3v1, ID3v1.1, Lyrics3v1, Lyrics3v2, ID3v2.2, ID3v2.3, and ID3v2.4 tags as well as reading MP3 Frame Headers:
private void printArtistFromMp3() {
final String DIRECTORY = "C:\\Freek\\Dropbox\\Freek\\Muziek\\Queens of the Stone Age\\2000 - R\\";
try {
final MP3File mp3File = new MP3File(DIRECTORY + "01-Feel good hit of the summer.mp3");
if (mp3File.hasID3v1Tag())
System.out.println("Artist (ID3v1 tag): " + mp3File.getID3v1Tag().getArtist());
if (mp3File.hasID3v2Tag())
System.out.println("Lead artist (ID3v2 tag): " + mp3File.getID3v2Tag().getLeadArtist());
} catch (IOException e) {
e.printStackTrace();
} catch (TagException e) {
e.printStackTrace();
}
}
The code above printed "Lead artist (ID3v2 tag): Queens of the Stone Age" on my laptop.
Take a look at Jaudiotagger:
Supports MP3 ID3v1,ID3v11, ID3v2.2, v2.3 and v2.4 are transparently

Categories