├── .gitignore ├── LICENSE ├── README.md ├── images └── ExtendedCsvDataSetConfig.png ├── pom.xml └── src └── main └── java └── com └── di └── jmeter ├── config ├── ExtendedCsvDataSetConfig.java └── gui │ └── ExtendedCsvDataSetConfigGui.java └── utils ├── BrowseAction.java └── FileServerExtended.java /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | target 4 | dependency-reduced-pom.xml 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Mohamed Ibrahim 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extended-csv-dataset-config 2 | 3 | ## Introduction 4 | 5 | This plugin provides additional feature over the JMeter's default **CSV data set config element**. This also provides additional parameterization feature. 6 | 7 | This will enable **LoadRunner** users, the privilege of having similar parameter advantage in **Apache JMeter** 8 | 9 | ## Preview 10 | ![Extended CSV Dataset Config](/images/ExtendedCsvDataSetConfig.png) 11 | 12 | ## Required Components 13 | 14 | 1. Apache JMeter components 15 | 2. Apache JMeter core 16 | 17 | ## Jmeter Target 18 | 19 | * Jmeter version 5.2.1 or above 20 | * Java 8 or above 21 | 22 | ## Installation Instructions 23 | 24 | * Download the source code from the GitHub. 25 | * Just do a mvn clean install (M2 is required) 26 | * Jar will be generated under the target directory (di-extended-csv-xx.jar). 27 | * Copy the Jar to \/lib/ext/ 28 | 29 | ## What's new ? 30 | 31 | * Improved new GUI 32 | * Added feature to create new file 33 | * Added feature to edit csv file with default text editor 34 | * Fixed quoted data issue 35 | * Fixed relative path issue 36 | * Support for large csv (Moved out of In-memory read) 37 | 38 | 39 | ## Options 40 | 41 | ✨ This version eliminates remembering the below combination table ✨ 42 | 43 | This allows reading of CSV data as follows 44 | 45 | * Select Row (Sequential | Random | Unique) 46 | * Update Value (Each Iteration | Once) 47 | * When Out of Values (Continue Cyclic | Continue with last Value | Abort Thread) 48 | 49 | The below table is the combinations allowed while using this plugin 50 | 51 | | Select Row | Update value | Out of Values | Allocate Block Size | 52 | |------------|----------------|--------------------------|---------------------| 53 | | Sequential | Each Iteration | Continue Cyclic | NA | 54 | | Sequential | Each Iteration | Abort Thread | NA | 55 | | Sequential | Each Iteration | Continue with Last value | NA | 56 | | Sequential | Once | NA | NA | 57 | | Random | Each Iteration | NA | NA | 58 | | Random | Once | NA | NA | 59 | | Unique | Each Iteration | Continue with Last Value | Enabled | 60 | | Unique | Each Iteration | Continue Cyclic | Enabled | 61 | | Unique | Each Iteration | Abort Thread | Enabled | 62 | | Unique | Once | NA | NA | 63 | 64 | 65 | ## Future Release in pipeline 66 | 67 | * Visualizing csv data in data table 68 | * Simulate Parameter window 69 | 70 | 71 | ## References 72 | 73 | * CSV data set config 74 | 75 | 76 | ## 💲 Support Me 77 | 78 | If this project help you reduce time to develop, you can give me a cup of coffee :) 79 | 80 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://ko-fi.com/rollno748) 81 | 82 | Please rate a star(:star2:) - If you like it. 83 | 84 | Please open up a bug(:beetle:) - If you experience abnormalities. 85 | 86 | -------------------------------------------------------------------------------- /images/ExtendedCsvDataSetConfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rollno748/Extended-csv-dataset-config/4feaddec770299d5f6e40389a0628f44b4df172d/images/ExtendedCsvDataSetConfig.png -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | com.di.jmeter.ecsv 6 | di-extended-csv 7 | 2.3 8 | 9 | 10 | 5.1.1 11 | 1.8 12 | 1.8 13 | 1.8 14 | 3.2.2 15 | 2.8.0 16 | 3.11 17 | 18 | 19 | 20 | 21 | org.apache.jmeter 22 | ApacheJMeter_core 23 | ${jmeter-version} 24 | 25 | 26 | org.apache.jmeter 27 | ApacheJMeter_components 28 | ${jmeter-version} 29 | 30 | 31 | org.apache.commons 32 | commons-lang3 33 | ${commons-lang3-version} 34 | 35 | 36 | 37 | 38 | 39 | 40 | src/main/java 41 | 42 | **/*.properties 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.apache.maven.plugins 50 | maven-compiler-plugin 51 | 2.3 52 | 53 | ${jdk.version} 54 | ${jdk.version} 55 | true 56 | true 57 | true 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /src/main/java/com/di/jmeter/config/ExtendedCsvDataSetConfig.java: -------------------------------------------------------------------------------- 1 | package com.di.jmeter.config; 2 | 3 | import com.di.jmeter.utils.FileServerExtended; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.apache.jmeter.JMeter; 6 | import org.apache.jmeter.config.ConfigTestElement; 7 | import org.apache.jmeter.engine.event.LoopIterationEvent; 8 | import org.apache.jmeter.engine.event.LoopIterationListener; 9 | import org.apache.jmeter.engine.util.NoConfigMerge; 10 | import org.apache.jmeter.gui.GuiPackage; 11 | import org.apache.jmeter.save.CSVSaveService; 12 | import org.apache.jmeter.testelement.TestStateListener; 13 | import org.apache.jmeter.threads.JMeterContext; 14 | import org.apache.jmeter.threads.JMeterContextService; 15 | import org.apache.jmeter.threads.JMeterVariables; 16 | import org.apache.jmeter.util.JMeterUtils; 17 | import org.apache.jorphan.util.JMeterStopThreadException; 18 | import org.apache.jorphan.util.JOrphanUtils; 19 | import org.slf4j.Logger; 20 | import org.slf4j.LoggerFactory; 21 | 22 | import java.io.IOException; 23 | 24 | public class ExtendedCsvDataSetConfig extends ConfigTestElement implements LoopIterationListener, TestStateListener, NoConfigMerge { 25 | private static final long serialVersionUID = 767792680142202807L; 26 | private static final Logger LOGGER = LoggerFactory.getLogger(ExtendedCsvDataSetConfig.class); 27 | 28 | private static final String EOFVALUE = JMeterUtils.getPropDefault("csvdataset.eofstring", ""); 29 | public static final String FILENAME = "filename"; 30 | public static final String FILE_ENCODING = "fileEncoding"; 31 | public static final String VARIABLE_NAMES = "variableNames"; 32 | public static final String DELIMITER = "delimiter"; 33 | public static final String IGNORE_FIRST_LINE = "ignoreFirstLine"; 34 | public static final String QUOTED_DATA = "quotedData"; 35 | public static final String SELECT_ROW = "selectRow"; 36 | public static final String UPDATE_VALUE = "updateValue"; 37 | public static final String OO_VALUE = "ooValue"; 38 | public static final String SHARE_MODE = "shareMode"; 39 | public static final String AUTO_ALLOCATE = "autoAllocate"; 40 | public static final String ALLOCATE = "allocate"; 41 | public static final String BLOCK_SIZE = "blockSize"; 42 | private String[] variables; 43 | private String alias; 44 | private boolean recycleFile; 45 | private boolean ignoreFirstLine; 46 | private boolean updateOnceFlag = true; 47 | 48 | @Override 49 | public void iterationStart(LoopIterationEvent iterationEvent) { 50 | final JMeterContext context = getThreadContext(); 51 | final String delimiter = this.getDelimiter(); 52 | FileServerExtended fileServer = FileServerExtended.getFileServer(); 53 | String[] lineValues = {}; 54 | if (variables == null) { 55 | FileServerExtended.setReadPos(0); 56 | initVars(fileServer, context, delimiter); 57 | } 58 | JMeterVariables jMeterVariables = context.getVariables(); 59 | // Select Row -> Sequential, Random, Unique 60 | switch(getSelectRow().toLowerCase()){ 61 | case "sequential": 62 | try{ 63 | String line = fileServer.readLine(alias, recycleFile, ignoreFirstLine); 64 | LOGGER.debug("Sequential line fetched : {}", line); 65 | if(isQuotedData()){ 66 | lineValues = fileServer.csvReadLine(line, delimiter.charAt(0)); 67 | }else{ 68 | lineValues = JOrphanUtils.split(line, delimiter, false); 69 | } 70 | }catch(IOException e){ 71 | LOGGER.error(e.toString()); 72 | } 73 | break; 74 | case "random": 75 | try{ 76 | String line = fileServer.readRandom(alias, ignoreFirstLine); 77 | LOGGER.debug("Random line fetched : {}", line); 78 | if(isQuotedData()){ 79 | lineValues = fileServer.csvReadLine(line, delimiter.charAt(0)); 80 | }else{ 81 | lineValues = JOrphanUtils.split(line, delimiter, false); 82 | } 83 | }catch(IOException e){ 84 | LOGGER.error(e.toString()); 85 | } 86 | break; 87 | case "unique": 88 | try{ 89 | String line = fileServer.readUnique(alias, ignoreFirstLine, getOoValue(), FileServerExtended.getReadPos(), FileServerExtended.getStartPos(), FileServerExtended.getEndPos()); 90 | LOGGER.debug("Unique line fetched : {}", line); 91 | if(isQuotedData()){ 92 | lineValues = fileServer.csvReadLine(line, delimiter.charAt(0)); 93 | }else{ 94 | lineValues = JOrphanUtils.split(line, delimiter, false); 95 | } 96 | }catch(IOException e){ 97 | LOGGER.error(e.toString()); 98 | } 99 | break; 100 | default: 101 | throw new IllegalStateException("Unexpected value: " + getSelectRow().toLowerCase()); 102 | } 103 | 104 | // Update Value --> Each Iteration, Once 105 | switch (getPropertyAsString(UPDATE_VALUE).toLowerCase()) { 106 | case "each iteration": 107 | if (lineValues.length == 0) { 108 | throw new JMeterStopThreadException("End of file:"+ getFilename()+" detected for CSV DataSet:" 109 | +getName()+" configured to Select Row Parameter :" + getUpdateValue()); 110 | } else { 111 | for (int a = 0; a < variables.length && a < lineValues.length; a++) { 112 | jMeterVariables.put(variables[a], lineValues[a]); 113 | } 114 | } 115 | break; 116 | case "once": 117 | if(updateOnceFlag){ 118 | for (int a = 0; a < variables.length && a < lineValues.length; a++) { 119 | jMeterVariables.put(variables[a], lineValues[a]); 120 | } 121 | this.updateOnceFlag = false; 122 | } 123 | break; 124 | default: 125 | LOGGER.error("Invalid selection on Update Value"); 126 | throw new JMeterStopThreadException("Invalid selection :" + getFilename() + " detected for Extended CSV DataSet:" 127 | + getName() + " configured to Select Row Parameter :" + getUpdateValue()); 128 | } 129 | } 130 | 131 | private void initVars(FileServerExtended fileServer, JMeterContext context, String delimiter) { 132 | String fileName = getFilename().trim(); 133 | final String varNames = getVariableNames(); 134 | setAlias(context, fileName); 135 | this.ignoreFirstLine = this.isIgnoreFirstLine(); 136 | 137 | if(getOoValue() != null && getOoValue().equalsIgnoreCase("Continue Cyclic")){ 138 | this.recycleFile = true; 139 | } 140 | if (StringUtils.isEmpty(varNames)) { 141 | String header = fileServer.reserveFile(fileName, getFileEncoding(), alias, true); 142 | try { 143 | variables = CSVSaveService.csvSplitString(header, delimiter.charAt(0)); 144 | ignoreFirstLine = true; 145 | trimVarNames(variables); 146 | } catch (IOException e) { 147 | throw new IllegalArgumentException("Could not split CSV header line from file:" + fileName, e); 148 | } 149 | }else{ 150 | fileServer.reserveFile(fileName, getFileEncoding(), alias, isIgnoreFirstLine()); 151 | variables = JOrphanUtils.split(varNames, ","); 152 | } 153 | 154 | if(!getSelectRow().equalsIgnoreCase("Sequential")){ 155 | fileServer.calculateRowCount(alias, getVariableNames().isEmpty() && isIgnoreFirstLine()); 156 | } 157 | if(getSelectRow().equalsIgnoreCase("Unique")){ 158 | this.initBlockFeatures(context); 159 | } 160 | trimVarNames(variables); 161 | } 162 | 163 | private void initBlockFeatures(JMeterContext context) { 164 | String threadName = context.getThread().getThreadName(); 165 | int blockSize; 166 | 167 | if(isAutoAllocate()){ 168 | blockSize = FileServerExtended.getRowCount() / JMeterContextService.getTotalThreads(); 169 | }else{ 170 | blockSize = Integer.parseInt(getBlockSize()); 171 | if(blockSize < 1){ 172 | throw new JMeterStopThreadException("Block Size Allocation Exception :" + getBlockSize() + " Please Ensure the block size is greater than 0" 173 | + " Or select auto allocate feature, which is currently set to : " + isAutoAllocate()); 174 | } 175 | } 176 | //Set Start and end position to block 177 | FileServerExtended.setReadPosition(threadName, blockSize, isIgnoreFirstLine()); 178 | if(FileServerExtended.getReadPos() == 0){ 179 | FileServerExtended.setReadPos(FileServerExtended.getStartPos()); 180 | } 181 | } 182 | 183 | private void setAlias(final JMeterContext context, String alias) { 184 | switch (getShareMode()) { 185 | case "All threads": 186 | this.alias = alias; 187 | break; 188 | case "Current thread group": 189 | this.alias = alias + "@" + System.identityHashCode(context.getThreadGroup()); 190 | break; 191 | case "Current thread": 192 | this.alias = alias + "@" + System.identityHashCode(context.getThread()); 193 | break; 194 | default: 195 | this.alias = alias + "@" + getShareMode(); 196 | break; 197 | } 198 | if(getSelectRow().equalsIgnoreCase("Sequential")){ 199 | this.alias = alias + "@" + System.identityHashCode(context.getThread()); 200 | } 201 | } 202 | 203 | private boolean isServerMode() { 204 | return System.getProperty("server_port") != null; 205 | } 206 | 207 | @Override 208 | public void testStarted() { 209 | FileServerExtended fileServer = FileServerExtended.getFileServer(); 210 | String baseDirectory = org.apache.jmeter.services.FileServer.getFileServer().getBaseDir(); 211 | 212 | if(JMeter.isNonGUI() || isServerMode() || GuiPackage.getInstance() == null){ 213 | fileServer.setBasedir(baseDirectory); 214 | } else { 215 | String testPlanFile = GuiPackage.getInstance().getTestPlanFile(); 216 | fileServer.setBasedir(testPlanFile); 217 | } 218 | } 219 | 220 | @Override 221 | public void testEnded() { 222 | FileServerExtended fileServerExtended = FileServerExtended.getFileServer(); 223 | try{ 224 | fileServerExtended.closeFiles(); 225 | } catch (IOException e){ 226 | LOGGER.error("Exception occurred while closing file(s) : {}", e.toString()); 227 | } 228 | } 229 | 230 | @Override 231 | public void testStarted(String host) { 232 | testStarted(); 233 | } 234 | 235 | @Override 236 | public void testEnded(String host) { 237 | testEnded(); 238 | } 239 | 240 | /** 241 | * trim content of array varNames 242 | * @param varsNames - Variable names 243 | */ 244 | private void trimVarNames(String[] varsNames) { 245 | for (int i = 0; i < varsNames.length; i++) { 246 | varsNames[i] = varsNames[i].trim(); 247 | } 248 | } 249 | 250 | /** 251 | * Getters and setters for the Config variables 252 | */ 253 | public String getFilename() { 254 | return getPropertyAsString(FILENAME); 255 | } 256 | public void setFilename(String filename) { 257 | setProperty(FILENAME, filename); 258 | } 259 | public String getFileEncoding() { 260 | return getPropertyAsString(FILE_ENCODING); 261 | } 262 | public void setFileEncoding(String fileEncoding) { 263 | setProperty(FILE_ENCODING, fileEncoding); 264 | } 265 | public String getVariableNames() { 266 | return getPropertyAsString(VARIABLE_NAMES); 267 | } 268 | public void setVariableNames(String variableNames) { 269 | setProperty(VARIABLE_NAMES, variableNames); 270 | } 271 | public boolean isIgnoreFirstLine() { 272 | return getPropertyAsBoolean(IGNORE_FIRST_LINE); 273 | } 274 | public void setIgnoreFirstLine(Boolean selectedItem) { 275 | setProperty(IGNORE_FIRST_LINE, selectedItem); 276 | } 277 | public String getDelimiter() { 278 | return getPropertyAsString(DELIMITER); 279 | } 280 | public void setDelimiter(String delimiter) { 281 | String delim; 282 | if ("\\t".equals(delimiter)) { 283 | delim = "\t"; 284 | } else if (delimiter.isEmpty()){ 285 | LOGGER.debug("Empty delimiter, ',' (Comma) will be used by default"); 286 | delim = ","; 287 | }else{ 288 | delim = delimiter; 289 | } 290 | setProperty(DELIMITER, delim); 291 | } 292 | public boolean isQuotedData() { 293 | return getPropertyAsBoolean(QUOTED_DATA); 294 | } 295 | public void setQuotedData(Boolean selectedItem) { 296 | setProperty(QUOTED_DATA, selectedItem); 297 | } 298 | public String getSelectRow() { 299 | return getPropertyAsString(SELECT_ROW); 300 | } 301 | public void setSelectRow(String selectRow) { 302 | setProperty(SELECT_ROW, selectRow); 303 | } 304 | public String getUpdateValue() { 305 | return getPropertyAsString(UPDATE_VALUE); 306 | } 307 | public void setUpdateValue(String updateValue) { 308 | setProperty(UPDATE_VALUE, updateValue); 309 | } 310 | public String getOoValue() { 311 | return getPropertyAsString(OO_VALUE); 312 | } 313 | public void setOoValue(String ooValue) { 314 | setProperty(OO_VALUE, ooValue); 315 | } 316 | public void setShareMode(String mode){ 317 | setProperty(SHARE_MODE, mode); 318 | } 319 | public String getShareMode() { 320 | return getPropertyAsString(SHARE_MODE); 321 | } 322 | public boolean isAutoAllocate() { 323 | return getPropertyAsBoolean(AUTO_ALLOCATE); 324 | } 325 | public void setAutoAllocate(boolean autoAllocate) { 326 | setProperty(AUTO_ALLOCATE, autoAllocate); 327 | } 328 | public boolean isAllocate() { 329 | return getPropertyAsBoolean(ALLOCATE); 330 | } 331 | public void setAllocate(boolean allocate) { 332 | setProperty(ALLOCATE, allocate); 333 | } 334 | public String getBlockSize() { 335 | return getPropertyAsString(BLOCK_SIZE); 336 | } 337 | public void setBlockSize(String blockSize) { 338 | setProperty(BLOCK_SIZE, blockSize); 339 | } 340 | public String printAllProperties() { 341 | return String.format("Filename: %s\n,FileEncoding: %s\n VariableName: %s\n IgnoreFirstLine: %s\n Delimiter: %s\n IsQuotedData: %s\n SelectRow: %s\n UpdateValue: %s\n OOValue: %s\n AutoAllocate: %s\n Allocate: %s\n BlockSize: %s\n",getFilename(),getFileEncoding(),getVariableNames(),isIgnoreFirstLine(),getDelimiter(),isQuotedData(),getSelectRow(),getUpdateValue(),getOoValue(),isAllocate(),isAutoAllocate(),getBlockSize()); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/main/java/com/di/jmeter/config/gui/ExtendedCsvDataSetConfigGui.java: -------------------------------------------------------------------------------- 1 | package com.di.jmeter.config.gui; 2 | 3 | import com.di.jmeter.config.ExtendedCsvDataSetConfig; 4 | import com.di.jmeter.utils.BrowseAction; 5 | import org.apache.jmeter.config.gui.AbstractConfigGui; 6 | import org.apache.jmeter.testelement.TestElement; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import javax.swing.*; 11 | import java.awt.*; 12 | import java.io.File; 13 | import java.io.FileNotFoundException; 14 | import java.io.IOException; 15 | import java.util.Objects; 16 | 17 | 18 | public class ExtendedCsvDataSetConfigGui extends AbstractConfigGui { 19 | private static final long serialVersionUID = 240L; 20 | private static final Logger LOGGER = LoggerFactory.getLogger(ExtendedCsvDataSetConfigGui.class); 21 | private static final String DISPLAY_NAME="Extended CSV Data Set Config"; 22 | private JTextField filenameField; 23 | private JComboBox fileEncodingCBox; 24 | private JTextField variableNamesField; 25 | private JComboBox ignoreFirstLineCBox; 26 | private JTextField delimiterField; 27 | private JComboBox quotedDataCBox; 28 | private JComboBox sharingModeCBox; 29 | private JComboBox selectRowCBox; 30 | private JComboBox updateValueCBox; 31 | private JComboBox ooValueCBox; 32 | private JRadioButton autoAllocateRButton; 33 | private JRadioButton allocateRButton; 34 | private JTextField blockSizeField; 35 | private final String[] fileEncodingValues = {"UTF-8", "UTF-16", "ISO-8859-15", "US-ASCII"}; 36 | private final String[] sharingModeValues = {"All threads", "Current thread group", "Current thread"}; 37 | private final String[] selectRowValues = {"Sequential", "Random", "Unique"}; 38 | private final String[] updateValues = {"Each Iteration", "Once"}; 39 | private final String[] ooValues = {"Continue Cyclic", "Continue with Last Value", "Abort Thread"}; 40 | 41 | public ExtendedCsvDataSetConfigGui(){ 42 | init(); 43 | initGuiValues(); 44 | } 45 | 46 | @Override 47 | public String getLabelResource() { 48 | return DISPLAY_NAME; 49 | } 50 | 51 | @Override 52 | public String getStaticLabel() { 53 | return DISPLAY_NAME; 54 | } 55 | 56 | @Override 57 | public TestElement createTestElement() { 58 | ExtendedCsvDataSetConfig element = new ExtendedCsvDataSetConfig(); 59 | modifyTestElement(element); 60 | return element; 61 | } 62 | 63 | @Override 64 | public void modifyTestElement(TestElement element) { 65 | configureTestElement(element); 66 | if(element instanceof ExtendedCsvDataSetConfig){ 67 | ExtendedCsvDataSetConfig eCsvDataSetConfig = (ExtendedCsvDataSetConfig) element; 68 | eCsvDataSetConfig.setFilename(this.filenameField.getText()); 69 | eCsvDataSetConfig.setFileEncoding(this.fileEncodingCBox.getItemAt(fileEncodingCBox.getSelectedIndex())); 70 | eCsvDataSetConfig.setVariableNames(this.variableNamesField.getText()); 71 | eCsvDataSetConfig.setIgnoreFirstLine(this.ignoreFirstLineCBox.getItemAt(ignoreFirstLineCBox.getSelectedIndex())); 72 | eCsvDataSetConfig.setDelimiter(this.delimiterField.getText()); 73 | eCsvDataSetConfig.setQuotedData(this.quotedDataCBox.getItemAt(quotedDataCBox.getSelectedIndex())); 74 | eCsvDataSetConfig.setSelectRow(this.selectRowCBox.getItemAt(selectRowCBox.getSelectedIndex())); 75 | eCsvDataSetConfig.setUpdateValue(this.updateValueCBox.getItemAt(updateValueCBox.getSelectedIndex())); 76 | eCsvDataSetConfig.setOoValue(this.ooValueCBox.getItemAt(ooValueCBox.getSelectedIndex())); 77 | eCsvDataSetConfig.setShareMode(this.sharingModeCBox.getItemAt(sharingModeCBox.getSelectedIndex())); 78 | eCsvDataSetConfig.setAutoAllocate(this.autoAllocateRButton.isSelected()); 79 | eCsvDataSetConfig.setAllocate(this.allocateRButton.isSelected()); 80 | eCsvDataSetConfig.setBlockSize(this.blockSizeField.getText()); 81 | LOGGER.debug("{}", eCsvDataSetConfig.printAllProperties()); 82 | } 83 | } 84 | 85 | private void init() { 86 | setLayout(new BorderLayout(0, 5)); 87 | setBorder(makeBorder()); 88 | Container topPanel = makeTitlePanel(); 89 | add(topPanel, BorderLayout.NORTH); 90 | 91 | JPanel rootPanel = new JPanel(new BorderLayout()); 92 | Box csvDataSourceConfigBox = Box.createVerticalBox(); 93 | JPanel csvDatasourceConfigPanel = new JPanel(new BorderLayout()); 94 | JPanel csvDataSourcePanel = new JPanel(new GridBagLayout()); 95 | csvDataSourcePanel.setBorder(BorderFactory.createTitledBorder("Configure the CSV Data Source")); //$NON-NLS-1$ 96 | 97 | GridBagConstraints labelConstraints = new GridBagConstraints(); 98 | labelConstraints.anchor = GridBagConstraints.FIRST_LINE_END; 99 | 100 | GridBagConstraints editConstraints = new GridBagConstraints(); 101 | editConstraints.anchor = GridBagConstraints.FIRST_LINE_START; 102 | editConstraints.weightx = 1.0; 103 | editConstraints.fill = GridBagConstraints.HORIZONTAL; 104 | 105 | int row = 0; 106 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("Filename: ", JLabel.RIGHT)); 107 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, filenameField = new JTextField(20)); 108 | JButton browseButton; 109 | JButton viewFileButton; 110 | addToPanel(csvDataSourcePanel, labelConstraints, 2, row, browseButton = new JButton("...")); 111 | addToPanel(csvDataSourcePanel, labelConstraints, 3, row, viewFileButton = new JButton("Edit")); 112 | row++; 113 | stretchItemToComponent(filenameField, browseButton); 114 | labelConstraints.insets = new java.awt.Insets(2, 0, 0, 0); 115 | editConstraints.insets = new java.awt.Insets(2, 0, 0, 0); 116 | browseButton.addActionListener(new BrowseAction(filenameField, false)); 117 | 118 | editConstraints.insets = new Insets(0, 5, 0, 0); 119 | browseButton.setPreferredSize(new Dimension(30, filenameField.getPreferredSize().height)); 120 | editConstraints.anchor = GridBagConstraints.BASELINE; 121 | 122 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("File encoding: ", JLabel.CENTER)); 123 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, fileEncodingCBox = new JComboBox<>(fileEncodingValues)); 124 | row++; 125 | 126 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("Variable Name(s): ", JLabel.CENTER)); 127 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, variableNamesField = new JTextField(30)); 128 | row++; 129 | 130 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("Consider first line as Variable Name: ", JLabel.CENTER)); 131 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, ignoreFirstLineCBox = new JComboBox<>(new Boolean[] { true, false })); 132 | row++; 133 | 134 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("Delimiter: ", JLabel.CENTER)); 135 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, delimiterField = new JTextField(20)); 136 | row++; 137 | 138 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("Allow Quoted Data: ", JLabel.CENTER)); 139 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, quotedDataCBox = new JComboBox<>(new Boolean[] { true, false })); 140 | row++; 141 | 142 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("Select Row: ", JLabel.CENTER)); 143 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, selectRowCBox = new JComboBox<>(selectRowValues)); 144 | row++; 145 | 146 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("Update Values: ", JLabel.CENTER)); 147 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, updateValueCBox = new JComboBox<>(updateValues)); 148 | row++; 149 | 150 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("When out of Values: ", JLabel.CENTER)); 151 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, ooValueCBox = new JComboBox<>(ooValues)); 152 | row++; 153 | 154 | addToPanel(csvDataSourcePanel, labelConstraints, 0, row, new JLabel("Sharing Mode: ", JLabel.CENTER)); 155 | addToPanel(csvDataSourcePanel, editConstraints, 1, row, sharingModeCBox = new JComboBox<>(sharingModeValues)); 156 | 157 | fileEncodingCBox.setEditable(true); 158 | csvDatasourceConfigPanel.add(csvDataSourcePanel, BorderLayout.NORTH); 159 | add(csvDatasourceConfigPanel, BorderLayout.CENTER); 160 | csvDataSourceConfigBox.add(csvDatasourceConfigPanel); 161 | 162 | Box allocateBlockConfigBox = Box.createVerticalBox(); 163 | JPanel allocateBlockConfigBoxPanel = new JPanel(new BorderLayout()); 164 | JPanel allocateConfigPanel = new JPanel(new GridBagLayout()); 165 | allocateConfigPanel.setBorder(BorderFactory.createTitledBorder("Allocate values to thread")); 166 | allocateConfigPanel.setLayout(new BoxLayout(allocateConfigPanel, BoxLayout.Y_AXIS)); 167 | 168 | autoAllocateRButton = new JRadioButton(); 169 | JLabel autoAllocateLabel = new JLabel("Automatically allocate block for threads"); 170 | JPanel radioButtonPanel1 = new JPanel(); 171 | radioButtonPanel1.setLayout(new FlowLayout(FlowLayout.LEFT)); 172 | radioButtonPanel1.add(autoAllocateRButton); 173 | radioButtonPanel1.add(autoAllocateLabel); 174 | radioButtonPanel1.setEnabled(true); 175 | allocateConfigPanel.add(radioButtonPanel1); 176 | 177 | allocateRButton = new JRadioButton(); 178 | JLabel allocateLabel1 = new JLabel("Allocate"); 179 | blockSizeField = new JTextField(3); 180 | JLabel allocateLabel2 = new JLabel(" values for each thread"); 181 | JPanel radioButtonPanel2 = new JPanel(); 182 | radioButtonPanel2.setLayout(new FlowLayout(FlowLayout.LEFT)); 183 | radioButtonPanel2.add(allocateRButton); 184 | radioButtonPanel2.add(allocateLabel1); 185 | radioButtonPanel2.add(blockSizeField); 186 | radioButtonPanel2.add(allocateLabel2); 187 | allocateConfigPanel.add(radioButtonPanel2); 188 | 189 | ButtonGroup allocationGroup = new ButtonGroup(); 190 | allocationGroup.add(autoAllocateRButton); 191 | allocationGroup.add(allocateRButton); 192 | 193 | allocateBlockConfigBoxPanel.add(allocateConfigPanel, BorderLayout.NORTH); 194 | add(allocateBlockConfigBoxPanel, BorderLayout.CENTER); 195 | allocateBlockConfigBox.add(allocateBlockConfigBoxPanel); 196 | 197 | ooValueCBox.setEnabled(true); 198 | allocateConfigPanel.setEnabled(false); 199 | allocateBlockConfigBoxPanel.setEnabled(false); 200 | autoAllocateRButton.setSelected(true); 201 | autoAllocateRButton.setEnabled(false); 202 | autoAllocateLabel.setEnabled(false); 203 | allocateRButton.setEnabled(false); 204 | allocateLabel1.setEnabled(false); 205 | allocateLabel2.setEnabled(false); 206 | allocateRButton.setSelected(false); 207 | /* 208 | * tooltip goes here 209 | */ 210 | filenameField.setToolTipText("Name of the file that holds the csv data (relative or absolute filename)"); 211 | browseButton.setToolTipText("Browse..."); 212 | viewFileButton.setToolTipText("View/Edit file in system default editor"); 213 | fileEncodingCBox.setToolTipText("The character set encoding used in the file"); 214 | variableNamesField.setToolTipText("List your variable names in order to match the order of columns in your csv data. Keep it empty to use the first line of the file for variable names"); 215 | ignoreFirstLineCBox.setToolTipText("Ignore first line of CSV file, it will only be used used if Variable Names is not empty, if Variable Names is empty the first line must contain the headers"); 216 | delimiterField.setToolTipText("Enter the delimiter ('\\t' for tab)"); 217 | quotedDataCBox.setToolTipText("Allow CSV data values to be quoted?"); 218 | selectRowCBox.setToolTipText("Options on reading file"); 219 | updateValueCBox.setToolTipText("Options on update parameter during run"); 220 | ooValueCBox.setToolTipText("Options while reaching EOF"); 221 | sharingModeCBox.setToolTipText("Select which threads share the same file pointer"); 222 | autoAllocateRButton.setToolTipText("Automatically calculates the blocksize for each thread(s)"); 223 | allocateRButton.setToolTipText("Custom allocation of blocksize for each thread(s)"); 224 | blockSizeField.setToolTipText("Blocksize value for custom allocation of threads"); 225 | 226 | rootPanel.add(csvDataSourceConfigBox, BorderLayout.NORTH); 227 | rootPanel.add(allocateBlockConfigBox, BorderLayout.CENTER); 228 | add(rootPanel,BorderLayout.CENTER); 229 | 230 | ignoreFirstLineCBox.addActionListener(e-> LOGGER.debug("Ignore First line in csv is set as : {}", ignoreFirstLineCBox.getSelectedItem())); 231 | quotedDataCBox.addActionListener(e-> LOGGER.debug("Quoted data in csv is set as : {}", quotedDataCBox.getSelectedItem())); 232 | sharingModeCBox.addActionListener(e-> LOGGER.debug("Sharing mode is set as : {}", sharingModeCBox.getSelectedItem())); 233 | 234 | selectRowCBox.addActionListener(e -> { 235 | LOGGER.debug("Selection is : {}", selectRowCBox.getSelectedItem()); 236 | if(Objects.equals(selectRowCBox.getSelectedItem(), "Unique")){ 237 | ooValueCBox.setEnabled(true); 238 | allocateConfigPanel.setEnabled(true); 239 | autoAllocateLabel.setEnabled(true); 240 | allocateLabel1.setEnabled(true); 241 | allocateLabel2.setEnabled(true); 242 | autoAllocateRButton.setEnabled(true); 243 | allocateRButton.setEnabled(true); 244 | autoAllocateRButton.setSelected(autoAllocateRButton.isSelected()); 245 | allocateRButton.setSelected(allocateRButton.isSelected()); 246 | blockSizeField.setEnabled(allocateRButton.isSelected() && allocateRButton.isEnabled()); 247 | }else if(Objects.equals(selectRowCBox.getSelectedItem(), "Sequential")){ 248 | ooValueCBox.setEnabled(true); 249 | allocateConfigPanel.setEnabled(false); 250 | autoAllocateLabel.setEnabled(false); 251 | allocateLabel1.setEnabled(false); 252 | allocateLabel2.setEnabled(false); 253 | autoAllocateRButton.setEnabled(false); 254 | allocateRButton.setEnabled(false); 255 | autoAllocateRButton.setSelected(autoAllocateRButton.isSelected()); 256 | allocateRButton.setSelected(allocateRButton.isSelected()); 257 | blockSizeField.setEnabled(allocateRButton.isSelected() && allocateRButton.isEnabled()); 258 | }else{ 259 | ooValueCBox.setEnabled(false); 260 | allocateConfigPanel.setEnabled(false); 261 | autoAllocateLabel.setEnabled(false); 262 | allocateLabel1.setEnabled(false); 263 | allocateLabel2.setEnabled(false); 264 | autoAllocateRButton.setEnabled(false); 265 | allocateRButton.setEnabled(false); 266 | blockSizeField.setEnabled(false); 267 | } 268 | }); 269 | 270 | autoAllocateRButton.addActionListener(e -> { 271 | autoAllocateRButton.setEnabled(autoAllocateRButton.isSelected()); 272 | allocateRButton.setEnabled(true); 273 | blockSizeField.setEnabled(false); 274 | }); 275 | 276 | allocateRButton.addActionListener(e -> { 277 | allocateLabel2.setEnabled(true); 278 | blockSizeField.setEnabled(allocateRButton.isSelected()); 279 | }); 280 | 281 | viewFileButton.addActionListener(e -> { 282 | try { 283 | File file = new File(filenameField.getText()); 284 | Desktop desktop = Desktop.getDesktop(); 285 | if(filenameField.getText().isEmpty() || filenameField.getText().isEmpty()){ 286 | throw new FileNotFoundException(); 287 | } 288 | if(!file.exists()){ 289 | int selection = JOptionPane.showConfirmDialog(new ExtendedCsvDataSetConfigGui(), "File does not exist. Do you want to create it ?", 290 | "File not Found", JOptionPane.YES_NO_OPTION); 291 | if(selection == JOptionPane.YES_OPTION){ 292 | file.createNewFile(); 293 | } 294 | } 295 | if(file.exists()){ 296 | if(desktop.isSupported(Desktop.Action.EDIT)){ 297 | desktop.edit(new File(filenameField.getText())); 298 | }else if(desktop.isSupported(Desktop.Action.OPEN)){ 299 | desktop.open(new File(filenameField.getText())); 300 | }else{ 301 | JOptionPane.showMessageDialog(new ExtendedCsvDataSetConfigGui(), "Unable to get the default editor"); 302 | } 303 | } 304 | } catch (FileNotFoundException fne){ 305 | JOptionPane.showMessageDialog(new ExtendedCsvDataSetConfigGui(),"Invalid File path."); 306 | } catch (IOException ex) { 307 | throw new RuntimeException(ex); 308 | } 309 | }); 310 | } 311 | 312 | private void initGuiValues() { 313 | filenameField.setText(""); 314 | fileEncodingCBox.setSelectedIndex(0); 315 | variableNamesField.setText(""); 316 | ignoreFirstLineCBox.setSelectedIndex(0); 317 | delimiterField.setText(","); 318 | quotedDataCBox.setSelectedIndex(1); 319 | selectRowCBox.setSelectedIndex(0); 320 | updateValueCBox.setSelectedIndex(0); 321 | ooValueCBox.setSelectedIndex(0); 322 | sharingModeCBox.setSelectedIndex(0); 323 | blockSizeField.setText(""); 324 | } 325 | 326 | @Override 327 | public void configure(TestElement element) { 328 | super.configure(element); 329 | if(element instanceof ExtendedCsvDataSetConfig){ 330 | ExtendedCsvDataSetConfig config = (ExtendedCsvDataSetConfig) element; 331 | filenameField.setText(config.getFilename()); 332 | fileEncodingCBox.setSelectedItem(config.getFileEncoding()); 333 | variableNamesField.setText(config.getVariableNames()); 334 | ignoreFirstLineCBox.setSelectedItem(config.isIgnoreFirstLine()); 335 | delimiterField.setText(config.getDelimiter()); 336 | quotedDataCBox.setSelectedItem(config.isQuotedData()); 337 | selectRowCBox.setSelectedItem(config.getSelectRow()); 338 | updateValueCBox.setSelectedItem(config.getUpdateValue()); 339 | ooValueCBox.setSelectedItem(config.getOoValue()); 340 | sharingModeCBox.setSelectedItem(config.getShareMode()); 341 | autoAllocateRButton.setSelected(config.isAutoAllocate()); 342 | allocateRButton.setSelected(config.isAllocate()); 343 | blockSizeField.setText(config.getBlockSize()); 344 | } 345 | } 346 | 347 | @Override 348 | public void clearGui() { 349 | super.clearGui(); 350 | initGuiValues(); 351 | } 352 | 353 | private void addToPanel(JPanel panel, GridBagConstraints constraints, int col, int row, JComponent component) { 354 | constraints.gridx = col; 355 | constraints.gridy = row; 356 | panel.add(component, constraints); 357 | } 358 | public static void stretchItemToComponent(JComponent component, JComponent item) { 359 | int iWidth = (int) item.getPreferredSize().getWidth(); 360 | int iHeight = (int) component.getPreferredSize().getHeight(); 361 | item.setPreferredSize(new Dimension(iWidth, iHeight)); 362 | } 363 | 364 | } 365 | -------------------------------------------------------------------------------- /src/main/java/com/di/jmeter/utils/BrowseAction.java: -------------------------------------------------------------------------------- 1 | package com.di.jmeter.utils; 2 | 3 | import org.apache.jmeter.gui.GuiPackage; 4 | 5 | import javax.swing.JFileChooser; 6 | import javax.swing.JTextField; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | 10 | public class BrowseAction implements ActionListener { 11 | private final JTextField control; 12 | private boolean isDirectoryBrowse; 13 | private String lastPath = "."; 14 | 15 | public BrowseAction(JTextField filename, boolean isDirectoryBrowse) { 16 | control = filename; 17 | this.isDirectoryBrowse = isDirectoryBrowse; 18 | } 19 | 20 | @Override 21 | public void actionPerformed(ActionEvent e) { 22 | JFileChooser chooser = getFileChooser(); 23 | if (chooser != null) { 24 | if(GuiPackage.getInstance() != null) { 25 | int returnVal = chooser.showOpenDialog(GuiPackage.getInstance().getMainFrame()); 26 | if(returnVal == JFileChooser.APPROVE_OPTION) { 27 | control.setText(chooser.getSelectedFile().getPath()); 28 | } 29 | lastPath = chooser.getCurrentDirectory().getPath(); 30 | } 31 | } 32 | } 33 | 34 | protected JFileChooser getFileChooser() { 35 | JFileChooser ret = new JFileChooser(lastPath); 36 | if(isDirectoryBrowse) { 37 | ret.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); 38 | } 39 | return ret; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/di/jmeter/utils/FileServerExtended.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to you under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | package com.di.jmeter.utils; 18 | 19 | import org.apache.commons.io.input.BOMInputStream; 20 | import org.apache.jmeter.gui.JMeterFileFilter; 21 | import org.apache.jmeter.save.CSVSaveService; 22 | import org.apache.jmeter.util.JMeterUtils; 23 | import org.apache.jorphan.util.JMeterStopThreadException; 24 | import org.apache.jorphan.util.JOrphanUtils; 25 | import org.slf4j.Logger; 26 | import org.slf4j.LoggerFactory; 27 | 28 | import java.io.*; 29 | import java.nio.file.Files; 30 | import java.nio.file.Paths; 31 | import java.util.*; 32 | import java.util.concurrent.ThreadLocalRandom; 33 | import java.util.stream.Stream; 34 | 35 | /** 36 | * This class provides thread-safe access to files, and to 37 | * provide some simplifying assumptions about where to find files and how to 38 | * name them. For instance, putting supporting files in the same directory as 39 | * the saved test plan file allows users to refer to the file with just it's 40 | * name - this FileServer class will find the file without a problem. 41 | * Eventually, I want all in-test file access to be done through here, with the 42 | * goal of packaging up entire test plans as a directory structure that can be 43 | * sent via rmi to remote servers (currently, one must make sure the remote 44 | * server has all support files in a relative-same location) and to package up 45 | * test plans to execute on unknown boxes that only have Java installed. 46 | */ 47 | public class FileServerExtended { 48 | 49 | private static final Logger log = LoggerFactory.getLogger(FileServerExtended.class); 50 | 51 | /** 52 | * The default base used for resolving relative files, i.e.
53 | * {@code System.getProperty("user.dir")} 54 | */ 55 | private static final String DEFAULT_BASE = System.getProperty("user.dir");// $NON-NLS-1$ 56 | 57 | /** Default base prefix: {@value} */ 58 | private static final String BASE_PREFIX_DEFAULT = "~/"; // $NON-NLS-1$ 59 | 60 | private static final String BASE_PREFIX = 61 | JMeterUtils.getPropDefault("jmeter.save.saveservice.base_prefix", // $NON-NLS-1$ 62 | BASE_PREFIX_DEFAULT); 63 | 64 | private File base; 65 | private static int rowCount; 66 | private static final ThreadLocal endPos = new ThreadLocal<>(); 67 | private static final ThreadLocal startPos = new ThreadLocal<>(); 68 | private static final ThreadLocal readPos = new ThreadLocal<>(); 69 | private final Map files = new HashMap<>(); 70 | private static final FileServerExtended server = new FileServerExtended(); 71 | 72 | // volatile needed to ensure safe publication 73 | private volatile String scriptName; 74 | 75 | // Cannot be instantiated 76 | private FileServerExtended() { 77 | base = new File(DEFAULT_BASE); 78 | log.info("Default base='{}'", DEFAULT_BASE); 79 | } 80 | 81 | /** 82 | * @return the singleton instance of the server. 83 | */ 84 | public static FileServerExtended getFileServer() { 85 | return server; 86 | } 87 | 88 | /** 89 | * Resets the current base to DEFAULT_BASE. 90 | */ 91 | public synchronized void resetBase() { 92 | checkForOpenFiles(); 93 | base = new File(DEFAULT_BASE); 94 | log.info("Reset base to '{}'", base); 95 | } 96 | 97 | /** 98 | * Sets the current base directory for relative file names from the provided path. 99 | * If the path does not refer to an existing directory, then its parent is used. 100 | * Normally the provided path is a file, so using the parent directory is appropriate. 101 | * 102 | * @param basedir the path to set, or {@code null} if the GUI is being cleared 103 | * @throws IllegalStateException if files are still open 104 | */ 105 | public synchronized void setBasedir(String basedir) { 106 | checkForOpenFiles(); // TODO should this be called if basedir == null? 107 | if (basedir != null) { 108 | File newBase = new File(basedir); 109 | if (!newBase.isDirectory()) { 110 | newBase = newBase.getParentFile(); 111 | } 112 | base = newBase; 113 | log.info("Set new base='{}'", base); 114 | } 115 | } 116 | 117 | /** 118 | * Sets the current base directory for relative file names from the provided script file. 119 | * The parameter is assumed to be the path to a JMX file, so the base directory is derived 120 | * from its parent. 121 | * 122 | * @param scriptPath the path of the script file; must be not be {@code null} 123 | * @throws IllegalStateException if files are still open 124 | * @throws IllegalArgumentException if scriptPath parameter is null 125 | */ 126 | public synchronized void setBaseForScript(File scriptPath) { 127 | if (scriptPath == null){ 128 | throw new IllegalArgumentException("scriptPath must not be null"); 129 | } 130 | setScriptName(scriptPath.getName()); 131 | // getParentFile() may not work on relative paths 132 | setBase(scriptPath.getAbsoluteFile().getParentFile()); 133 | } 134 | 135 | /** 136 | * Sets the current base directory for relative file names. 137 | * 138 | * @param jmxBase the path of the script file base directory, cannot be null 139 | * @throws IllegalStateException if files are still open 140 | * @throws IllegalArgumentException if {@code basepath} is null 141 | */ 142 | public synchronized void setBase(File jmxBase) { 143 | if (jmxBase == null) { 144 | throw new IllegalArgumentException("jmxBase must not be null"); 145 | } 146 | checkForOpenFiles(); 147 | base = jmxBase; 148 | log.info("Set new base='{}'", base); 149 | } 150 | 151 | /** 152 | * Check if there are entries in use. 153 | *

154 | * Caller must ensure that access to the files map is single-threaded as 155 | * there is a window between checking the files Map and clearing it. 156 | * 157 | * @throws IllegalStateException if there are any entries still in use 158 | */ 159 | private void checkForOpenFiles() throws IllegalStateException { 160 | if (filesOpen()) { // checks for entries in use 161 | throw new IllegalStateException("Files are still open, cannot change base directory"); 162 | } 163 | files.clear(); // tidy up any unused entries 164 | } 165 | 166 | public synchronized String getBaseDir() { 167 | return base.getAbsolutePath(); 168 | } 169 | 170 | public static String getDefaultBase(){ 171 | return DEFAULT_BASE; 172 | } 173 | 174 | /** 175 | * Calculates the relative path from DEFAULT_BASE to the current base, 176 | * which must be the same as or a child of the default. 177 | * 178 | * @return the relative path, or {@code "."} if the path cannot be determined 179 | */ 180 | public synchronized File getBaseDirRelative() { 181 | // Must first convert to absolute path names to ensure parents are available 182 | File parent = new File(DEFAULT_BASE).getAbsoluteFile(); 183 | File f = base.getAbsoluteFile(); 184 | ArrayDeque l = new ArrayDeque<>(); 185 | while (f != null) { 186 | if (f.equals(parent)){ 187 | if (l.isEmpty()){ 188 | break; 189 | } 190 | File rel = new File(l.pop()); 191 | while(!l.isEmpty()) { 192 | rel = new File(rel, l.pop()); 193 | } 194 | return rel; 195 | } 196 | l.push(f.getName()); 197 | f = f.getParentFile(); 198 | } 199 | return new File("."); 200 | } 201 | 202 | /** 203 | * Creates an association between a filename and a File inputOutputObject, 204 | * and stores it for later use - unless it is already stored. 205 | * 206 | * @param filename - relative (to base) or absolute file name (must not be null) 207 | */ 208 | public void reserveFile(String filename) { 209 | reserveFile(filename,null); 210 | } 211 | 212 | /** 213 | * Creates an association between a filename and a File inputOutputObject, 214 | * and stores it for later use - unless it is already stored. 215 | * 216 | * @param filename - relative (to base) or absolute file name (must not be null) 217 | * @param charsetName - the character set encoding to use for the file (perhaps null) 218 | */ 219 | public void reserveFile(String filename, String charsetName) { 220 | reserveFile(filename, charsetName, filename, false); 221 | } 222 | 223 | /** 224 | * Creates an association between a filename and a File inputOutputObject, 225 | * and stores it for later use - unless it is already stored. 226 | * 227 | * @param filename - relative (to base) or absolute file name (must not be null) 228 | * @param charsetName - the character set encoding to use for the file (perhaps null) 229 | * @param alias - the name to be used to access the object (must not be null) 230 | */ 231 | public void reserveFile(String filename, String charsetName, String alias) { 232 | reserveFile(filename, charsetName, alias, false); 233 | } 234 | 235 | /** 236 | * Creates an association between a filename and a File inputOutputObject, 237 | * and stores it for later use - unless it is already stored. 238 | * 239 | * @param filename - relative (to base) or absolute file name (must not be null or empty) 240 | * @param charsetName - the character set encoding to use for the file (perhaps null) 241 | * @param alias - the name to be used to access the object (must not be null) 242 | * @param hasHeader true if the file has a header line describing the contents 243 | * @return the header line; may be null 244 | * @throws IllegalArgumentException if header could not be read or filename is null or empty 245 | */ 246 | public synchronized String reserveFile(String filename, String charsetName, String alias, boolean hasHeader) { 247 | if (filename == null || filename.isEmpty()){ 248 | throw new IllegalArgumentException("Filename must not be null or empty"); 249 | } 250 | if (alias == null){ 251 | throw new IllegalArgumentException("Alias must not be null"); 252 | } 253 | FileEntry fileEntry = files.get(alias); 254 | if (fileEntry == null) { 255 | fileEntry = new FileEntry(resolveFileFromPath(filename), null, charsetName); 256 | if (filename.equals(alias)){ 257 | log.info("Stored: {}", filename); 258 | } else { 259 | log.info("Stored: {} Alias: {}", filename, alias); 260 | } 261 | files.put(alias, fileEntry); 262 | if (hasHeader) { 263 | try { 264 | fileEntry.headerLine = readLine(alias, false); 265 | if (fileEntry.headerLine == null) { 266 | fileEntry.exception = new EOFException("File is empty: " + fileEntry.file); 267 | } 268 | } catch (IOException | IllegalArgumentException e) { 269 | fileEntry.exception = e; 270 | } 271 | } 272 | } 273 | if (hasHeader && fileEntry.headerLine == null) { 274 | throw new IllegalArgumentException("Could not read file header line for file " + filename, 275 | fileEntry.exception); 276 | } 277 | return fileEntry.headerLine; 278 | } 279 | 280 | /** 281 | * Resolves file name into {@link File} instance. 282 | * When filename is not absolute and not found from current working dir, 283 | * it tries to find it under current base directory 284 | * @param filename original file name 285 | * @return {@link File} instance 286 | */ 287 | private File resolveFileFromPath(String filename) { 288 | File f = new File(filename); 289 | if (f.isAbsolute() || f.exists()) { 290 | return f; 291 | } else { 292 | return new File(base, filename); 293 | } 294 | } 295 | 296 | /** 297 | * Get the next line of the named file, recycle by default. 298 | * 299 | * @param filename the filename or alias that was used to reserve the file 300 | * @return String containing the next line in the file 301 | * @throws IOException when reading of the file fails, or the file was not reserved properly 302 | */ 303 | public String readLine(String filename) throws IOException { 304 | return readLine(filename, true); 305 | } 306 | 307 | /** 308 | * Get the next line of the named file, first line is name to false 309 | * 310 | * @param filename the filename or alias that was used to reserve the file 311 | * @param recycle - should file be restarted at EOF? 312 | * @return String containing the next line in the file (null if EOF reached and not recycle) 313 | * @throws IOException when reading of the file fails, or the file was not reserved properly 314 | */ 315 | public String readLine(String filename, boolean recycle) throws IOException { 316 | return readLine(filename, recycle, false); 317 | } 318 | 319 | /** 320 | * Get the next line of the named file 321 | * 322 | * @param filename the filename or alias that was used to reserve the file 323 | * @param recycle - should file be restarted at EOF? 324 | * @param ignoreFirstLine - Ignore first line 325 | * @return String containing the next line in the file (null if EOF reached and not recycle) 326 | * @throws IOException when reading of the file fails, or the file was not reserved properly 327 | */ 328 | public synchronized String readLine(String filename, boolean recycle, 329 | boolean ignoreFirstLine) throws IOException { 330 | FileEntry fileEntry = files.get(filename); 331 | if (fileEntry != null) { 332 | if (fileEntry.inputOutputObject == null) { 333 | fileEntry.inputOutputObject = createBufferedReader(fileEntry); 334 | } else if (!(fileEntry.inputOutputObject instanceof Reader)) { 335 | throw new IOException("File " + filename + " already in use"); 336 | } 337 | BufferedReader reader = (BufferedReader) fileEntry.inputOutputObject; 338 | String line = reader.readLine(); 339 | if (line == null && recycle) { 340 | reader.close(); 341 | reader = createBufferedReader(fileEntry); 342 | fileEntry.inputOutputObject = reader; 343 | if (ignoreFirstLine) { 344 | // read first line and forget 345 | reader.readLine();//NOSONAR 346 | } 347 | line = reader.readLine(); 348 | } 349 | return line; 350 | } 351 | throw new IOException("File never reserved: "+filename); 352 | } 353 | 354 | /** 355 | * 356 | * @param alias the file name or alias 357 | * @param ignoreFirstLine whether the file contains a file header which will be ignored 358 | * @param delim the delimiter to use for parsing 359 | * @return the parsed line, will be empty if the file is at EOF 360 | * @throws IOException when reading of the aliased file fails, or the file was not reserved properly 361 | */ 362 | public synchronized String[] getParsedLine(String alias, boolean recycle, boolean ignoreFirstLine, char delim) throws IOException { 363 | BufferedReader reader = getReader(alias, recycle, ignoreFirstLine); 364 | return CSVSaveService.csvReadFile(reader, delim); 365 | } 366 | 367 | /** 368 | * Return BufferedReader handling close if EOF reached and recycle is true 369 | * and ignoring first line if ignoreFirstLine is true 370 | * 371 | * @param alias String alias 372 | * @param recycle Recycle at eof 373 | * @param ignoreFirstLine Ignore first line 374 | * @return {@link BufferedReader} 375 | */ 376 | private BufferedReader getReader(String alias, boolean recycle, boolean ignoreFirstLine) throws IOException { 377 | FileEntry fileEntry = files.get(alias); 378 | if (fileEntry != null) { 379 | BufferedReader reader; 380 | if (fileEntry.inputOutputObject == null) { 381 | reader = createBufferedReader(fileEntry); 382 | fileEntry.inputOutputObject = reader; 383 | if (ignoreFirstLine) { 384 | // read first line and forget 385 | reader.readLine(); //NOSONAR 386 | } 387 | } else if (!(fileEntry.inputOutputObject instanceof Reader)) { 388 | throw new IOException("File " + alias + " already in use"); 389 | } else { 390 | reader = (BufferedReader) fileEntry.inputOutputObject; 391 | if (recycle) { // need to check if we are at EOF already 392 | reader.mark(1); 393 | int peek = reader.read(); 394 | if (peek == -1) { // already at EOF 395 | reader.close(); 396 | reader = createBufferedReader(fileEntry); 397 | fileEntry.inputOutputObject = reader; 398 | if (ignoreFirstLine) { 399 | // read first line and forget 400 | reader.readLine(); //NOSONAR 401 | } 402 | } else { // OK, we still have some data, restore it 403 | reader.reset(); 404 | } 405 | } 406 | } 407 | return reader; 408 | } else { 409 | throw new IOException("File never reserved: "+alias); 410 | } 411 | } 412 | 413 | private BufferedReader createBufferedReader(FileEntry fileEntry) throws IOException { 414 | if (!fileEntry.file.canRead() || !fileEntry.file.isFile()) { 415 | throw new IllegalArgumentException("File "+ fileEntry.file.getName()+ " must exist and be readable"); 416 | } 417 | BOMInputStream fis = new BOMInputStream(Files.newInputStream(fileEntry.file.toPath())); //NOSONAR 418 | InputStreamReader isr = null; 419 | // If file encoding is specified, read using that encoding, otherwise use default platform encoding 420 | String charsetName = fileEntry.charSetEncoding; 421 | if(!JOrphanUtils.isBlank(charsetName)) { 422 | isr = new InputStreamReader(fis, charsetName); 423 | } else if (fis.hasBOM()) { 424 | isr = new InputStreamReader(fis, fis.getBOM().getCharsetName()); 425 | } else { 426 | @SuppressWarnings("DefaultCharset") 427 | final InputStreamReader withPlatformEncoding = new InputStreamReader(fis); 428 | isr = withPlatformEncoding; 429 | } 430 | return new BufferedReader(isr); 431 | } 432 | 433 | public synchronized void write(String filename, String value) throws IOException { 434 | FileEntry fileEntry = files.get(filename); 435 | if (fileEntry != null) { 436 | if (fileEntry.inputOutputObject == null) { 437 | fileEntry.inputOutputObject = createBufferedWriter(fileEntry); 438 | } else if (!(fileEntry.inputOutputObject instanceof Writer)) { 439 | throw new IOException("File " + filename + " already in use"); 440 | } 441 | BufferedWriter writer = (BufferedWriter) fileEntry.inputOutputObject; 442 | log.debug("Write:{}", value); 443 | writer.write(value); 444 | } else { 445 | throw new IOException("File never reserved: "+filename); 446 | } 447 | } 448 | 449 | private BufferedWriter createBufferedWriter(FileEntry fileEntry) throws IOException { 450 | OutputStream fos = Files.newOutputStream(fileEntry.file.toPath()); 451 | OutputStreamWriter osw; 452 | // If file encoding is specified, write using that encoding, otherwise use default platform encoding 453 | String charsetName = fileEntry.charSetEncoding; 454 | if(!JOrphanUtils.isBlank(charsetName)) { 455 | osw = new OutputStreamWriter(fos, charsetName); 456 | } else { 457 | @SuppressWarnings("DefaultCharset") 458 | final OutputStreamWriter withPlatformEncoding = new OutputStreamWriter(fos); 459 | osw = withPlatformEncoding; 460 | } 461 | return new BufferedWriter(osw); 462 | } 463 | 464 | public synchronized void closeFiles() throws IOException { 465 | for (Map.Entry me : files.entrySet()) { 466 | closeFile(me.getKey(),me.getValue() ); 467 | } 468 | files.clear(); 469 | } 470 | 471 | /** 472 | * @param name the name or alias of the file to be closed 473 | * @throws IOException when closing of the aliased file fails 474 | */ 475 | public synchronized void closeFile(String name) throws IOException { 476 | FileEntry fileEntry = files.get(name); 477 | closeFile(name, fileEntry); 478 | } 479 | 480 | private void closeFile(String name, FileEntry fileEntry) throws IOException { 481 | if (fileEntry != null && fileEntry.inputOutputObject != null) { 482 | log.info("Close: {}", name); 483 | fileEntry.inputOutputObject.close(); 484 | fileEntry.inputOutputObject = null; 485 | } 486 | } 487 | 488 | boolean filesOpen() { // package access for test code only 489 | return files.values().stream() 490 | .anyMatch(fileEntry -> fileEntry.inputOutputObject != null); 491 | } 492 | 493 | /** 494 | * Method will get a random file in a base directory 495 | *

496 | * TODO hey, not sure this method belongs here. 497 | * FileServer is for thread safe File access relative to current test's base directory. 498 | * 499 | * @param basedir name of the directory in which the files can be found 500 | * @param extensions array of allowed extensions, if null is given, 501 | * any file be allowed 502 | * @return a random File from the basedir that matches one of 503 | * the extensions 504 | */ 505 | public File getRandomFile(String basedir, String[] extensions) { 506 | File input = null; 507 | if (basedir != null) { 508 | File src = new File(basedir); 509 | File[] lfiles = src.listFiles(new JMeterFileFilter(extensions)); 510 | if (lfiles != null) { 511 | // lfiles cannot be null as it has been checked before 512 | int count = lfiles.length; 513 | input = lfiles[ThreadLocalRandom.current().nextInt(count)]; 514 | } 515 | } 516 | return input; 517 | } 518 | 519 | /** 520 | * Get {@link File} instance for provided file path, 521 | * resolve file location relative to base dir or script dir when needed 522 | * 523 | * @param path original path to file, maybe relative 524 | * @return {@link File} instance 525 | */ 526 | public File getResolvedFile(String path) { 527 | reserveFile(path); 528 | return files.get(path).file; 529 | } 530 | 531 | private static class FileEntry{ 532 | private String headerLine; 533 | private Throwable exception; 534 | private final File file; 535 | private Closeable inputOutputObject; 536 | private final String charSetEncoding; 537 | 538 | FileEntry(File f, Closeable o, String e) { 539 | file = f; 540 | inputOutputObject = o; 541 | charSetEncoding = e; 542 | } 543 | } 544 | 545 | /** 546 | * Resolve a file name that may be relative to the base directory. If the 547 | * name begins with the value of the JMeter property 548 | * "jmeter.save.saveservice.base_prefix" - default "~/" - then the name is 549 | * assumed to be relative to the basename. 550 | * 551 | * @param relativeName 552 | * filename that should be checked for 553 | * jmeter.save.saveservice.base_prefix 554 | * @return the updated filename 555 | */ 556 | public static String resolveBaseRelativeName(String relativeName) { 557 | if (relativeName.startsWith(BASE_PREFIX)){ 558 | String newName = relativeName.substring(BASE_PREFIX.length()); 559 | return new File(getFileServer().getBaseDir(),newName).getAbsolutePath(); 560 | } 561 | return relativeName; 562 | } 563 | 564 | /** 565 | * @return JMX Script name 566 | * @since 2.6 567 | */ 568 | public String getScriptName() { 569 | return scriptName; 570 | } 571 | 572 | /** 573 | * @param scriptName Script name 574 | * @since 2.6 575 | */ 576 | public void setScriptName(String scriptName) { 577 | this.scriptName = scriptName; 578 | } 579 | 580 | /** 581 | * Get the number of rows count for the named file 582 | * 583 | * @param filename the filename or alias that was used to reserve the file 584 | * @param ignoreFirstLine Consider first line as variable name ? 585 | * @return String containing the next line in the file 586 | * @throws IOException when reading of the file fails, or the file was not reserved properly 587 | */ 588 | public void calculateRowCount(String filename, boolean ignoreFirstLine) { 589 | FileEntry fileEntry = files.get(filename); 590 | int count = 0; 591 | try (BufferedReader br = new BufferedReader(new FileReader(String.valueOf(fileEntry.file.toPath())))) { 592 | count = (int) br.lines().count(); 593 | } catch (IOException e) { 594 | log.error(e.toString()); 595 | 596 | } 597 | this.setRowCount(ignoreFirstLine ? count-1 : count ); 598 | } 599 | 600 | /** 601 | * Set the read position to Thread local (specific to each thread) 602 | * 603 | * @param threadName the filename or alias that was used to reserve the file 604 | * @param blockSize Set the block 605 | * @param ignoreFirstLine Consider first line as variable name ? 606 | * @return String containing the next line in the file 607 | * @throws IOException when reading of the file fails, or the file was not reserved properly 608 | */ 609 | public static void setReadPosition(String threadName, int blockSize, boolean ignoreFirstLine) { 610 | int head = ignoreFirstLine ? 1 : 0; 611 | int endPos = (Integer.parseInt(threadName.substring(threadName.lastIndexOf('-') + 1)) * blockSize) - 1; 612 | setEndPos(endPos); 613 | setStartPos((getEndPos() - blockSize) + 1); 614 | if(ignoreFirstLine){ 615 | setEndPos(getEndPos() + head); 616 | setStartPos(getStartPos() + head); 617 | } 618 | } 619 | 620 | /** 621 | * Get the random line using index of the named file 622 | * 623 | * @param filename the filename or alias that was used to reserve the file 624 | * @param ignoreFirstLine Consider first line as variable name ? 625 | * @return String containing the next line in the file 626 | * @throws IOException when reading of the file fails, or the file was not reserved properly 627 | */ 628 | public synchronized String readRandom(String filename, boolean ignoreFirstLine) throws IOException { 629 | Random rand = new Random(); 630 | int startPos = ignoreFirstLine ? 1 : 0; 631 | int randPos = rand.nextInt(((rowCount -1) - startPos) + 1) + startPos; 632 | return readIndexed(filename, randPos); 633 | } 634 | 635 | /** 636 | * Get the indexed line of the named file according to thread specific (Thread local) 637 | * 638 | * @param filename the filename or alias that was used to reserve the file 639 | * @param ignoreFirstLine Consider first line as variable name? 640 | * @param ooValue Out of value handler (recycle/abort thread/ Continue with last used value) 641 | * @param currPos - position in the allocated block (Read position - specific to thread local) 642 | * @param startPos - Starting position in the allocated block (start position - specific to thread local) 643 | * @param endPos - Ending position in the allocated block (End position - specific to thread local) 644 | * @return String containing the next line in the file 645 | * @throws IOException when reading of the file fails, or the file was not reserved properly 646 | */ 647 | public synchronized String readUnique(String filename, boolean ignoreFirstLine, String ooValue, int currPos, int startPos, int endPos) throws IOException { 648 | String line = null; 649 | if(currPos < getRowCount()){ 650 | line = readIndexed(filename, currPos); 651 | } 652 | if(ooValue.equalsIgnoreCase("Continue Cyclic")){ 653 | if(currPos >= endPos){ 654 | readPos.set(startPos); 655 | }else { 656 | readPos.set(currPos + 1); 657 | } 658 | }else if(ooValue.equalsIgnoreCase("Abort Thread")){ 659 | if(currPos <= endPos){ 660 | readPos.set(currPos + 1); 661 | }else{ 662 | throw new JMeterStopThreadException("End of Block :" + filename + " detected for Extended CSV DataSet:" 663 | + filename + " configured with stopThread: " + ooValue); 664 | } 665 | }else{ 666 | if(currPos >= endPos){ 667 | readPos.set(currPos); 668 | }else{ 669 | readPos.set(currPos + 1); 670 | } 671 | } 672 | return line; 673 | } 674 | 675 | /** 676 | * Get the indexed line of the named file 677 | * 678 | * @param filename the filename or alias that was used to reserve the file 679 | * @param pos - line number to fetch from the file (starts from 0) 680 | * @return String containing the next line in the file 681 | * @throws IOException when reading of the file fails, or the file was not reserved properly 682 | */ 683 | 684 | private String readIndexed(String filename, int pos) throws IOException { 685 | String line = null; 686 | FileEntry fileEntry = files.get(filename); 687 | if(fileEntry != null){ 688 | try (Stream lines = Files.lines(Paths.get(String.valueOf(fileEntry.file.toPath())))) { 689 | line = lines.skip(pos).findFirst().get(); 690 | }catch(IOException e){ 691 | log.error(e.toString()); 692 | } 693 | } 694 | return line; 695 | } 696 | 697 | private enum ParserState { 698 | INITIAL, 699 | PLAIN, 700 | QUOTED, 701 | EMBEDDEDQUOTE 702 | } 703 | private static final char QUOTING_CHAR = '"'; 704 | 705 | private static boolean isDelimOrEOL(char delim, int ch) { 706 | return ch == delim || ch == '\n' || ch == '\r'; 707 | } 708 | 709 | /** 710 | * Reads from file and splits input into strings according to the delimiter, 711 | * taking note of quoted strings. 712 | *

713 | * Handles DOS (CRLF), Unix (LF), and Mac (CR) line-endings equally. 714 | *

715 | * A blank line - or a quoted blank line - both return an array containing 716 | * a single empty String. 717 | * @param line String 718 | * @param delimiter delimiter (e.g. comma) 719 | * @return array of strings, will be empty if there is no data, i.e. if the input is at EOF. 720 | * @throws IOException 721 | * also for unexpected quote characters 722 | */ 723 | public String[] csvReadLine(String line, char delimiter) throws IOException { 724 | int index = 0; 725 | int length = line.length(); 726 | ParserState state = ParserState.INITIAL; 727 | List list = new ArrayList<>(); 728 | CharArrayWriter baos = new CharArrayWriter(200); 729 | boolean push = false; 730 | while (index < length) { 731 | push = false; 732 | int ch = line.charAt(index++); 733 | switch (state) { 734 | case INITIAL: 735 | if (ch == QUOTING_CHAR) { 736 | state = ParserState.QUOTED; 737 | } else if (isDelimOrEOL(delimiter, ch)) { 738 | push = true; 739 | } else { 740 | baos.write(ch); 741 | state = ParserState.PLAIN; 742 | } 743 | break; 744 | case PLAIN: 745 | if (ch == QUOTING_CHAR) { 746 | baos.write(ch); 747 | throw new IOException("Cannot have quote-char in plain field:[" + baos.toString() + "]"); 748 | } else if (isDelimOrEOL(delimiter, ch)) { 749 | push = true; 750 | state = ParserState.INITIAL; 751 | } else { 752 | baos.write(ch); 753 | } 754 | break; 755 | case QUOTED: 756 | if (ch == QUOTING_CHAR) { 757 | state = ParserState.EMBEDDEDQUOTE; 758 | } else { 759 | baos.write(ch); 760 | } 761 | break; 762 | case EMBEDDEDQUOTE: 763 | if (ch == QUOTING_CHAR) { 764 | baos.write(QUOTING_CHAR); // doubled quote => quote 765 | state = ParserState.QUOTED; 766 | } else if (isDelimOrEOL(delimiter, ch)) { 767 | push = true; 768 | state = ParserState.INITIAL; 769 | } else { 770 | baos.write(QUOTING_CHAR); 771 | throw new IOException("Cannot have single quote-char in quoted field:[" + baos.toString() + "]"); 772 | } 773 | break; 774 | default: 775 | throw new IllegalStateException("Unexpected state " + state); 776 | } 777 | if (push) { 778 | if (ch == '\r' && index < length && line.charAt(index) == '\n') { 779 | index++; // skip the '\n' character after '\r' 780 | } 781 | String s = baos.toString(); 782 | list.add(s); 783 | baos.reset(); 784 | } 785 | } 786 | if (state == ParserState.QUOTED) { 787 | throw new IOException("Missing trailing quote-char in quoted field:[\"" + baos.toString() + "\"]"); 788 | } 789 | // Do we have some data, or a trailing empty field? 790 | if (baos.size() > 0 // we have some data 791 | || push // we've started a field 792 | || state == ParserState.EMBEDDEDQUOTE // Just seen "" 793 | ) { 794 | list.add(baos.toString()); 795 | } 796 | return list.toArray(new String[list.size()]); 797 | } 798 | 799 | public static int getRowCount() { 800 | return rowCount; 801 | } 802 | public static void setRowCount(int count) { 803 | FileServerExtended.rowCount = count; 804 | } 805 | 806 | public static int getEndPos() { 807 | return endPos.get(); 808 | } 809 | public static void setEndPos(int ep) { 810 | endPos.set(ep); 811 | } 812 | public static int getStartPos() { 813 | return startPos.get(); 814 | } 815 | public static void setStartPos(int sp) { 816 | startPos.set(sp); 817 | } 818 | public static int getReadPos() { 819 | return readPos.get(); 820 | } 821 | public static void setReadPos(int rp) { 822 | readPos.set(rp); 823 | } 824 | } 825 | --------------------------------------------------------------------------------