Maximum length of JTextField and only accepts numbers? - email

I want the user to input a maximum of 8 numbers as it is a field for Mobile number.
This is my JTextField.
txtMobile = new JTextField();
txtMobile.setColumns(10);
txtMobile.setBounds(235, 345, 145, 25);
add(txtMobile);
While we're at it, how do I check for invalid characters like >> '^%$* in a JTextField?
1)Maximum Length
2)Accepts only numbers
3)Check for invalid characters
4)Check if it's a valid email address
Please help :D

You could use a JFormattedField, check out How to Use Formatted Text Fields, but they tend not to restrict the user from entering what ever they want, but instead does a post validation of the value to see if meets the needs of the format you sepcify
You could use a DocumentFilter instead, which would allow you to filter the input in real time.
Take a look at Implementing a Document Filter and MDP's Weblog for examples

Look at this sample example it will only accepts numbers as an input, as an answer to your second requirement.
public class InputInteger
{
private JTextField tField;
private JLabel label=new JLabel();
private MyDocumentFilter documentFilter;
private void displayGUI()
{
JFrame frame = new JFrame("Input Integer Example");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
JPanel contentPane = new JPanel();
contentPane.setBorder(
BorderFactory.createEmptyBorder(5, 5, 5, 5));
tField = new JTextField(10);
((AbstractDocument)tField.getDocument()).setDocumentFilter(
new MyDocumentFilter());
contentPane.add(tField);
contentPane.add(label);
frame.setContentPane(contentPane);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args)
{
Runnable runnable = new Runnable()
{
#Override
public void run()
{
new InputInteger().displayGUI();
}
};
EventQueue.invokeLater(runnable);
}
}
class MyDocumentFilter extends DocumentFilter{
private static final long serialVersionUID = 1L;
#Override
public void insertString(FilterBypass fb, int off
, String str, AttributeSet attr)
throws BadLocationException
{
// remove non-digits
fb.insertString(off, str.replaceAll("\\D++", ""), attr);
}
#Override
public void replace(FilterBypass fb, int off
, int len, String str, AttributeSet attr)
throws BadLocationException
{
// remove non-digits
fb.replace(off, len, str.replaceAll("\\D++", ""), attr);
}
}

Related

JLabel Not Showing When resetting JFrame to setVisible(true)

I have created an Abstract Class for common things to be useful in my entire program
public interface Global {
static final String regexdate="^(((0[1-9]|[12][0-9]|3[01])[- /.](0[13578]|1[02])|(0[1-9]|[12][0-9]|30)[- /.](0[469]|11)|(0[1-9]|1\\d|2[0-8])[- /.]02)[- /.]\\d{4}|29[- /.]02[- /.](\\d{2}(0[48]|[2468][048]|[13579][26])|([02468][048]|[1359][26])00))$";
static final String regexemail="\\b[\\w\\.-]+#[\\w\\.-]+\\.\\w{2,4}+\\b$";
static final String regexalpha="\\b[a-zA-Z .]+\\b$";
static final String regexnumbersonly="\\b[^a-zA-Z ,.+-]*\\d\\b$";
static final String regexalphaonly="\\b[a-zA-Z]+\\b$";
//address would allow any character case insensitive and numbers (for house number) "-" space "," and "/"
static final String regexaddress="\\b[a-zA-Z\\s 0-9-,/\\n]*\\b$";
static final String regexalphanumeric="\\b[a-zA-Z0-9 .]+\\b$";
static final String regexisbn="\\b[0-9-]+\\b$";
static final String regexdecimals="\\b[^a-zA-Z ,+-]*\\d\\b$";
// single character datatype validation regex
static final String regexchar="^[.a-zA-Z\s]+$";
static LocalDate localDate = LocalDate.now();
static DayOfWeek dayofweek=localDate.getDayOfWeek();
static int dayofmonth=localDate.getDayOfMonth();
static Month monthofyear=localDate.getMonth();
static int year=localDate.getYear();
static String properday=dayofweek.toString().substring(0,1)+dayofweek.toString().substring(1,3).toLowerCase();
static String propermonth=monthofyear.toString().substring(0, 1)+monthofyear.toString().substring(1, 3).toLowerCase();
static String dayfull=properday+" "+Integer.valueOf(dayofmonth)+"-"+propermonth+"-"+Integer.valueOf(year).toString().substring(2,4);
static final Font myfont=new Font("Serg UI",Font.PLAIN,16);
static final Font myfontheader=new Font("Serg UI",Font.BOLD,20);
static final Font myfonttoget=new Font("serg UI",Font.CENTER_BASELINE,16);
static final Color mybackground= new Color(231, 231, 231);
public JLabel title=new JLabel("Library Management System",SwingConstants.LEFT);
public JLabel welcome=new JLabel ("Welcome :-",SwingConstants.LEFT);
public JLabel ctandd=new JLabel(dayfull);
}
Now I have two separate classes where i am using all of above as required
admintitle.setBounds(0,0,900,49);
title.setBounds(310, 10, 335,20);
welcome.setBounds(1, 0, 130, 20);
ctandd.setBounds(790, 5,225, 20);
totalmem.setBounds(1, 25,100, 20);
adminpanel.setBackground(new Color(1,3,45));
JLabel addvalue=new JLabel("10");
addvalue.setFont(myfont);
addvalue.setForeground(Color.WHITE);
addvalue.setSize(20, 20);
today.add(addvalue);
lmember.setAlignmentX(Component.LEFT_ALIGNMENT);
separator.setAlignmentX(Component.LEFT_ALIGNMENT);
addmem.setAlignmentX(Component.CENTER_ALIGNMENT);
modifymem.setAlignmentX(Component.CENTER_ALIGNMENT);
delmem.setAlignmentX(Component.CENTER_ALIGNMENT);
admintitle.add(title);
admintitle.add(welcome);
admintitle.add(ctandd);
admintitle.add(totalmem);
adminpanel.add(lmember);
adminpanel.add(separator);
adminpanel.add(addmem);
adminpanel.add(modifymem);
adminpanel.add(delmem);
adminpanel.add(separator1);
adminpanel.add(lbook);
adminpanel.add(separator2);
adminpanel.add(addbook);
adminpanel.add(modifyinven);
adminpanel.add(delbook);
adminpanel.add(separator3);
adminpanel.add(lrepo);
adminpanel.add(penaltyrepo);
adminpanel.add(separator4);
adminpanel.add(newrequest);
adminpanel.add(exit);
admindisplay.add(totaldue);
admindisplay.add(totalpenalty);
admindisplay.add(penaltyamt);
admindisplay.add(memadded);
admindisplay.add(booksadded);
admindisplay.add(bookslost);
admindisplay.add(booksoverdue);
admindisplay.add(memdiscontinued);
admindisplay.add(newbookrequest);
admindisplay.add(memhead);
admindisplay.add(separator5);
admindisplay.add(bookhead);
admindisplay.add(separator6);
admindisplay.add(summary);
admindisplay.add(today);
admindisplay.add(weekly);
admindisplay.add(monthly);
admindisplay.add(yearly);
admin.add(admintitle);
admin.add(adminpanel);
admin.add(admindisplay);
admin.setVisible(true);
System.out.println(admin.getComponentCount());
System.out.println(admin.getComponentZOrder(title));
System.out.println(admindisplay.getComponentCount());
System.out.println(adminpanel.getComponentCount());
System.out.println(admintitle.getComponentCount());
} // end of admin ui
from this class when I click on a button I navigate to another class
addmem.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
// do login code
admin.setVisible(false);
uiaddmem.caddmemUI(admin);
}});
In another class I do something and upon user clicking back button I come back to this JFrame
back.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
modmember.setVisible(false);
admin.setVisible(true);
admin.toFront();
admin.validate();
System.out.println(admin.getComponentCount());
System.out.println(admin.getComponentZOrder(title));
System.out.println(admintitle.getComponentCount());
}});
Eveything works fine except that my title, welcome, total and ctandd just disappear from the first JFrame when I make admin.setVisible(true) as above.
As you can see I have done componentcount the numbers show that the three labels should be there also I individually tried sysout title.isDisplayable() title.isVisible()
title.isShowing() all of these are true but still the title, welcome, ctandd is not displayed when I return to my admin JFrame

Writable Classes in mapreduce

How can i use the values from hashset (the docid and offset) to the reduce writable so as to connect map writable with reduce writable?
The mapper (LineIndexMapper) works fine but in the reducer (LineIndexReducer) i get the error that it can't get string as argument when i type this:
context.write(key, new IndexRecordWritable("some string");
although i have the public String toString() in the ReduceWritable too.
I believe the hashset in reducer's writable (IndexRecordWritable.java) maybe isn't taking the values correctly?
I have the below code.
IndexMapRecordWritable.java
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
public class IndexMapRecordWritable implements Writable {
private LongWritable offset;
private Text docid;
public LongWritable getOffsetWritable() {
return offset;
}
public Text getDocidWritable() {
return docid;
}
public long getOffset() {
return offset.get();
}
public String getDocid() {
return docid.toString();
}
public IndexMapRecordWritable() {
this.offset = new LongWritable();
this.docid = new Text();
}
public IndexMapRecordWritable(long offset, String docid) {
this.offset = new LongWritable(offset);
this.docid = new Text(docid);
}
public IndexMapRecordWritable(IndexMapRecordWritable indexMapRecordWritable) {
this.offset = indexMapRecordWritable.getOffsetWritable();
this.docid = indexMapRecordWritable.getDocidWritable();
}
#Override
public String toString() {
StringBuilder output = new StringBuilder()
output.append(docid);
output.append(offset);
return output.toString();
}
#Override
public void write(DataOutput out) throws IOException {
}
#Override
public void readFields(DataInput in) throws IOException {
}
}
IndexRecordWritable.java
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.util.HashSet;
import org.apache.hadoop.io.Writable;
public class IndexRecordWritable implements Writable {
// Save each index record from maps
private HashSet<IndexMapRecordWritable> tokens = new HashSet<IndexMapRecordWritable>();
public IndexRecordWritable() {
}
public IndexRecordWritable(
Iterable<IndexMapRecordWritable> indexMapRecordWritables) {
}
#Override
public String toString() {
StringBuilder output = new StringBuilder();
return output.toString();
}
#Override
public void write(DataOutput out) throws IOException {
}
#Override
public void readFields(DataInput in) throws IOException {
}
}
Alright, here is my answer based on a few assumptions. The final output is a text file containing the key and the file names separated by a comma based on the information in the reducer class's comments on the pre-condition and post-condition.
In this case, you really don't need IndexRecordWritable class. You can simply write to your context using
context.write(key, new Text(valueBuilder.substring(0, valueBuilder.length() - 1)));
with the class declaration line as
public class LineIndexReducer extends Reducer<Text, IndexMapRecordWritable, Text, Text>
Don't forget to set the correct output class in the driver.
That must serve the purpose according to the post-condition in your reducer class. But, if you really want to write a Text-IndexRecordWritable pair to your context, there are two ways approach it -
with string as an argument (based on your attempt passing a string when you IndexRecordWritable class constructor is not designed to accept strings) and
with HashSet as an argument (based on the HashSet initialised in IndexRecordWritable class).
Since your constructor of IndexRecordWritable class is not designed to accept String as an input, you cannot pass a string. Hence the error you are getting that you can't use string as an argument. Ps: if you want your constructor to accept Strings, you must have another constructor in your IndexRecordWritable class as below:
// Save each index record from maps
private HashSet<IndexMapRecordWritable> tokens = new HashSet<IndexMapRecordWritable>();
// to save the string
private String value;
public IndexRecordWritable() {
}
public IndexRecordWritable(
HashSet<IndexMapRecordWritable> indexMapRecordWritables) {
/***/
}
// to accpet string
public IndexRecordWritable (String value) {
this.value = value;
}
but that won't be valid if you want to use the HashSet. So, approach #1 can't be used. You can't pass a string.
That leaves us with approach #2. Passing a HashSet as an argument since you want to make use of the HashSet. In this case, you must create a HashSet in your reducer before passing it as an argument to IndexRecordWritable in context.write.
To do this, your reducer must look like this.
#Override
protected void reduce(Text key, Iterable<IndexMapRecordWritable> values, Context context) throws IOException, InterruptedException {
//StringBuilder valueBuilder = new StringBuilder();
HashSet<IndexMapRecordWritable> set = new HashSet<>();
for (IndexMapRecordWritable val : values) {
set.add(val);
//valueBuilder.append(val);
//valueBuilder.append(",");
}
//write the key and the adjusted value (removing the last comma)
//context.write(key, new IndexRecordWritable(valueBuilder.substring(0, valueBuilder.length() - 1)));
context.write(key, new IndexRecordWritable(set));
//valueBuilder.setLength(0);
}
and your IndexRecordWritable.java must have this.
// Save each index record from maps
private HashSet<IndexMapRecordWritable> tokens = new HashSet<IndexMapRecordWritable>();
// to save the string
//private String value;
public IndexRecordWritable() {
}
public IndexRecordWritable(
HashSet<IndexMapRecordWritable> indexMapRecordWritables) {
/***/
tokens.addAll(indexMapRecordWritables);
}
Remember, this is not the requirement according to the description of your reducer where it says.
POST-CONDITION: emit the output a single key-value where all the file names are separated by a comma ",". <"marcello", "a.txt#3345,b.txt#344,c.txt#785">
If you still choose to emit (Text, IndexRecordWritable), remember to process the HashSet in IndexRecordWritable to get it in the desired format.

Why isn't my JFrame displaying anything from my Jpanel?

I'm working on creating a 3 man's morris board, but nothing is being displayed on the frame. It's empty despite having added my JPanel. Everything is fine if I used board = new JPanel(new GridLayout()); and do the following, but I wouldn't be able to draw the lines that would draw the board. I've looked over it a few times but can't seem to find a problem.
public class Project5 extends JFrame {
public final static int FRAME_WIDTH = 600;
public final static int FRAME_HEIGHT = 600;
private JButton jb[] = new JButton[9];
private Board board = new Board();
Project5(){
for(int i = 0; i<9; i++){
jb[i] = new JButton();
board.add(jb[i]);
}
add(board);
}
public static void main(String[] args) {
JFrame frame = new Project5();
frame.setTitle("Three Man's Morris");
frame.setSize(Project5.FRAME_WIDTH,Project5.FRAME_HEIGHT);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
}
class Board extends JPanel{
public Board(){
super();
setLayout(new GridLayout(3,0,Project5.FRAME_WIDTH,Project5.FRAME_HEIGHT));
}
#Override
public void paintComponents(Graphics g){
super.paintComponents(g);
g.drawLine(0, Project5.FRAME_WIDTH, 0, Project5.FRAME_HEIGHT);
g.drawLine(0, 0, 0, Project5.FRAME_HEIGHT);
g.drawLine(0,Project5.FRAME_WIDTH,0,0);
g.drawLine(0, Project5.FRAME_HEIGHT, Project5.FRAME_WIDTH, Project5.FRAME_HEIGHT);
g.drawLine(Project5.FRAME_WIDTH, 0, 0, Project5.FRAME_HEIGHT);
g.drawLine(Project5.FRAME_WIDTH,0,Project5.FRAME_WIDTH,Project5.FRAME_HEIGHT);
}
}
The problem is in your GridLayout() parameters :
GridLayout(rows,cols,horizontal_gap,vertical_gap)
in your case, both gaps are 600 (FRAME_WIDTH, FRAME_HEIGHT) !
The buttons are displayed, but they are outside the panel, try to lower the gap,
i.e. : setLayout(new GridLayout(3,0,0,0));
You should see the buttons.

Gwt Simple pager issues with a column sort handler

I have set up an AsyncDataProvider for my CellTable and added it to a SimplePager. I have hooked up a ListHandler to take care of sorting based on a column.
When I click the header of that column, the data doesn't change but on going to the next/previous page within the pager the data is then sorted. Also before the column is clicked there is no visual indicator on the column that would indicate that it is meant to be sortable.
How can I get the data to update when I click the header of the Column?
Here's my code snippet
service.getHosts(environment, new AsyncCallback<Set<String>>() {
#Override
public void onSuccess(final Set<String> hosts) {
final List<String> hostList = new ArrayList<String>(hosts);
//Populate the table
CellTable<String> hostTable = new CellTable<String>();
TextColumn<String> hostNameColumn = new TextColumn<String>(){
#Override
public String getValue(String string){
return string;
}
};
NumberCell numberCell = new NumberCell();
Column<String, Number> lengthColumn = new Column<String, Number>(numberCell){
#Override
public Number getValue(String string) {
return new Integer(string.length());
}
};
AsyncDataProvider<String> dataProvider = new AsyncDataProvider<String>() {
#Override
protected void onRangeChanged(HasData<String> data) {
int start = data.getVisibleRange().getStart();
int end = start + data.getVisibleRange().getLength();
List<String> subList = hostList.subList(start, end);
updateRowData(start, subList);
}
};
// Hooking up sorting
ListHandler<String> columnSortHandler = new ListHandler<String>(hostList);
columnSortHandler.setComparator(lengthColumn, new Comparator<String>(){
#Override
public int compare(String arg0, String arg1) {
return new Integer(arg0.length()).compareTo(arg1.length());
}
});
hostTable.setPageSize(10);
hostTable.addColumnSortHandler(columnSortHandler);
hostTable.addColumn(hostNameColumn,"Host Name");
lengthColumn.setSortable(true);
hostTable.addColumn(lengthColumn, "Length");
VerticalPanel verticalPanel = new VerticalPanel();
SimplePager pager = new SimplePager();
pager.setDisplay(hostTable);
dataProvider.addDataDisplay(hostTable);
dataProvider.updateRowCount(hosts.size(), true);
verticalPanel.add(hostTable);
verticalPanel.add(pager);
RootPanel.get().add(verticalPanel);
}
#Override
public void onFailure(Throwable throwable) {
Window.alert(throwable.getMessage());
}
});
I'm not sure how to make sure that the list is shared by both the table and the Pager. Before adding the pager I was using
ListDataProvider<String> dataProvider = new ListDataProvider<String>();
ListHandler<String> columnSortHandler = new ListHandler<String>(dataProvider.getList());
The AsyncDataProvider doesn't have the method getList.
To summarize I want the data to be sorted as soon as the column is clicked and not after I move forward/backward with the pager controls.
As per the suggestion I have changed the code for the AsyncDataProvider to
AsyncDataProvider<String> dataProvider = new AsyncDataProvider<String>() {
#Override
protected void onRangeChanged(HasData<String> data) {
int start = data.getVisibleRange().getStart();
int end = start + data.getVisibleRange().getLength();
List<String> subList = hostList.subList(start, end);
// Hooking up sorting
ListHandler<String> columnSortHandler = new ListHandler<String>(hostList);
hostTable.addColumnSortHandler(columnSortHandler);
columnSortHandler.setComparator(lengthColumn, new Comparator<String>(){
#Override
public int compare(String v0, String v1) {
return new Integer(v0.length).compareTo(v1.length);
}
});
updateRowData(start, subList);
}
};
But there is no change in the behavior even after that. Can someone please explain the process. The GWT showcase app seems to have this functionality but how they've done it isn't all that clear.
When using an AsyncDataProvider both pagination and sorting are meant to be done on the server side. You will need an AsyncHandler to go with your AsyncDataProvider:
AsyncHandler columnSortHandler = new AsyncHandler(dataGrid) {
#Override
public void onColumnSort(ColumnSortEvent event) {
#SuppressWarnings("unchecked")
int sortIndex = dataGrid.getColumnIndex((Column<Entry, ?>) event.getColumn());
boolean isAscending = event.isSortAscending();
service.getPage(0, sortIndex, isAscending, new AsyncCallback<List<Entry>>() {
public void onFailure(Throwable caught) {
}
public void onSuccess(List<Entry> result) {
pager.setPage(0);
provider.updateRowData(0, result);
}
});
}
};
dataGrid.addColumnSortHandler(columnSortHandler);
Clicking on a column header will then fire a columnSortEvent. Then you have to get the column clicked. I am overloading my servlet to provide both sorting and pagination, so I pass a -1 for the column index when only pagination is desired.
provider = new AsyncDataProvider<Entry>() {
#Override
protected void onRangeChanged(HasData<Entry> display) {
final int start = display.getVisibleRange().getStart();
service.getPage(start, -1, true, new AsyncCallback<List<Entry>>() {
#Override
public void onFailure(Throwable caught) {
}
#Override
public void onSuccess(List<Entry> result) {
provider.updateRowData(start, result);
}
});
}
};
provider.addDataDisplay(dataGrid);
provider.updateRowCount(0, true);
Then your servlet implementation of getPage performs the sorting and pagination. The whole thing is much easier to follow with separate event handlers.
I think the problem is with the ListHandler initialization. You are passing hostList as a parameter to List Handler and in onRangeChange method you are calling updateRowData with a different list (sublist).
Make sure you use the same list in both the places.
or
Move your ListHander initialization and cellTable.addColumnSortHandler method call to onRangeChange method after updateRowData call.

GWT-Editors and sub-editors

I'm trying to run an example of Editors with sub-editors.
When flushing the parent the value of child editor is null.
The classes are Person and Address.
The main editor is:
// editor fields
public TextField firstname;
public TextField lastname;
public NumberField<Integer> id;
public AddressEditor address = new AddressEditor();
public PersonEditor(Person p){
asWidget();
}
#Override
public Widget asWidget() {
setWidth(400);
setBodyStyle("padding: 5px;");
setHeaderVisible(false);
VerticalLayoutContainer c = new VerticalLayoutContainer();
id = new NumberField<Integer>(new IntegerPropertyEditor());
// id.setName("id");
id.setFormat(NumberFormat.getFormat("0.00"));
id.setAllowNegative(false);
c.add(new FieldLabel(id, "id"), new VerticalLayoutData(1, -1));
firstname = new TextField();
// firstname.setName("firstname");
c.add(new FieldLabel(firstname, "firstname"), new VerticalLayoutData(1, -1));
lastname = new TextField();
lastname.setName("lastname");
c.add(new FieldLabel(lastname, "lastname"), new VerticalLayoutData(1, -1));
c.add(address);
add(c);
return this;
The sub-editor:
public class AddressEditor extends Composite implements Editor<Address> {
private AddressProperties props = GWT.create(AddressProperties.class);
private ListStore<Address> store = new ListStore<Address>(props.key());
ComboBox<Address> address;
public AddressEditor() {
for(int i = 0; i < 5; i ++)
store.add(new Address("city" + i));
address = new ComboBox<Address>(store, props.nameLabel());
address.setAllowBlank(false);
address.setForceSelection(true);
address.setTriggerAction(TriggerAction.ALL);
initWidget(address);
}
And this is where the Driver is created:
private HorizontalPanel hp;
private Person googleContact;
PersonDriver driver = GWT.create(PersonDriver.class);
public void onModuleLoad() {
hp = new HorizontalPanel();
hp.setSpacing(10);
googleContact = new Person();
PersonEditor pe = new PersonEditor(googleContact);
driver.initialize(pe);
driver.edit(googleContact);
TextButton save = new TextButton("Save");
save.addSelectHandler(new SelectHandler() {
#Override
public void onSelect(SelectEvent event) {
googleContact = driver.flush();
System.out.println(googleContact.getFirstname() + ", " + googleContact.getAddress().getCity());
if (driver.hasErrors()) {
new MessageBox("Please correct the errors before saving.").show();
return;
}
}
});
The value of googleContact.getFirstname() is filled but googleContact.getAddress() is always null.
What I'm missing?
The AddressEditor needs to map to the Address model - presently, it doesn't seem to, unless Address only has one getter/setter, called getAddress() and setAddress(Address), which wouldn't really make a lot of sense.
If you want just a ComboBox<Address> (which implements Editor<Address> already), consider putting that combo in the PersonEditor directly. Otherwise, you'll need to add #Path("") to the AddressEditor.address field, to indicate that it should be directly editing the value itself, and not a sub property (i.e. person.getAddress().getAddress()).
Another way to build an address editor would be to list each of the properties of the Address type in the AddressEditor. This is what the driver is expecting by default, so it is confused when it sees a field called 'address'.
Two quick thoughts on the code itself: there is no need to pass a person into the PersonEditor - thats the job of the driver itself. Second, your editor fields do not need to be public, they just can't be private.